Skip to content

Commit

Permalink
#398 authentication support (用户注册不在scope)
Browse files Browse the repository at this point in the history
  • Loading branch information
kfchu committed Apr 15, 2018
1 parent 646f8c3 commit 786f58a
Show file tree
Hide file tree
Showing 15 changed files with 692 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.vip.saturn.job.console.controller.gui;

import com.vip.saturn.job.console.controller.SuccessResponseEntity;
import com.vip.saturn.job.console.domain.RequestResult;
import com.vip.saturn.job.console.exception.SaturnJobConsoleException;
import com.vip.saturn.job.console.mybatis.entity.User;
import com.vip.saturn.job.console.service.AuthenticationService;
import com.vip.saturn.job.console.utils.SaturnConsoleUtils;
import com.vip.saturn.job.console.utils.SessionAttributeKeys;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.slf4j.Logger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@RequestMapping("/console/authentication")
public class AuthenticationController extends AbstractGUIController {

private static final Logger AUDIT_LOGGER = SaturnConsoleUtils.getAuditLogger();

@Resource
private AuthenticationService authenticationService;

@ApiResponses(value = {@ApiResponse(code = 200, message = "Success/Fail", response = RequestResult.class)})
@RequestMapping(value = "/login", method = {RequestMethod.POST})
public SuccessResponseEntity login(String username, String password, HttpServletRequest request) throws IOException, SaturnJobConsoleException {

User user = authenticationService.authenticate(username, password);
if (user == null) {
throw new SaturnJobConsoleException("Invalid username or password");
}

request.getSession().setAttribute(SessionAttributeKeys.LOGIN_USER_REAL_NAME, user.getRealName());

AUDIT_LOGGER.info("{}({}) was login where ip={} ", user.getUserName(), user.getRealName(), request.getRemoteAddr());

return new SuccessResponseEntity();
}

@ApiResponses(value = {@ApiResponse(code = 200, message = "Success/Fail", response = RequestResult.class)})
@RequestMapping(value = "/logout", method = {RequestMethod.GET, RequestMethod.POST})
public SuccessResponseEntity logout(HttpServletRequest request) {
AUDIT_LOGGER.info("{}({}) logout, ip {}. ", getCurrentLoginUserName(), getCurrentLoginUserRealName(), request.getRemoteAddr());
request.getSession().invalidate();
return new SuccessResponseEntity();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.vip.saturn.job.console.filter;

import com.alibaba.fastjson.JSON;
import com.vip.saturn.job.console.domain.RequestResultHelper;
import com.vip.saturn.job.console.utils.SessionAttributeKeys;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class AuthenticationFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;

if (req.getSession().getAttribute(SessionAttributeKeys.LOGIN_USER_NAME) == null) {
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = resp.getWriter();
writer.print(JSON.toJSONString(RequestResultHelper.redirect("/login")));
writer.flush();
return;
}

chain.doFilter(request, response);
}

@Override
public void destroy() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.vip.saturn.job.console.service;

import com.vip.saturn.job.console.exception.SaturnJobConsoleException;
import com.vip.saturn.job.console.mybatis.entity.User;

public interface AuthenticationService {

User authenticate(String username, String password) throws SaturnJobConsoleException;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.vip.saturn.job.console.service.impl;

import com.vip.saturn.job.console.exception.SaturnJobConsoleException;
import com.vip.saturn.job.console.mybatis.entity.User;
import com.vip.saturn.job.console.mybatis.repository.UserRepository;
import com.vip.saturn.job.console.service.AuthenticationService;
import com.vip.saturn.job.console.utils.PasswordUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;

public class AuthenticationServiceImpl implements AuthenticationService {

@Autowired
private UserRepository userRepository;

@Value("${authentication.hash:plaintext}")
private String hashMethod;

@Override
public User authenticate(String username, String password) throws SaturnJobConsoleException {
if (StringUtils.isEmpty(password)) {
return null;
}

User user = userRepository.select(username);
if (user == null) {
return null;
}

try {
return PasswordUtils.validate(password, user.getPassword(), hashMethod) ? user : null;
} catch (Exception e) {
throw new SaturnJobConsoleException(e);
}
}

public void setHashMethod(String hashMethod) {
this.hashMethod = hashMethod;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.vip.saturn.job.console.utils;

import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;

public class PasswordUtils {

public static final String HASH_METHOD_PLANTEXT = "plaintext";

public static final String HASH_METHOD_PBKDF2 = "PBKDF2WithHmacSHA1";

private static final int ITERATIONS = 10 * 1000;

private static final int SALT_LEN = 8;

private static final int KEY_LEN = 256;

/**
* 生成带盐的密码串,密码和盐使用'$'符号分隔。
* @param hashMethod 为JDK SecretKeyFactory支持的算法,如果算法不存在,会使用PBKDF2WithHmacSHA1
*/
public static String genPassword(String password, String hashMethod) throws Exception {
byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(SALT_LEN);
return hash(password, salt, hashMethod);
}

public static String genPassword(String password, byte[] salt, String hashMethod) throws Exception {
return hash(password, salt, hashMethod) + "$" + Base64.encodeBase64String(salt);
}

public static String hash(String password, byte[] salt, String hashMethod) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory secretKeyFactory;
try {
secretKeyFactory = SecretKeyFactory.getInstance(hashMethod);
} catch (NoSuchAlgorithmException e) {
secretKeyFactory = SecretKeyFactory.getInstance(HASH_METHOD_PBKDF2);
}

SecretKey key = secretKeyFactory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LEN));
return Base64.encodeBase64String(key.getEncoded());
}

public static boolean validate(String password, String passwordInDB, String hashMethod) throws Exception {
if (PasswordUtils.HASH_METHOD_PLANTEXT.equals(hashMethod)) {
return password.equals(passwordInDB);
}

String[] saltAndPassword = passwordInDB.split("\\$");
if (saltAndPassword.length != 2) {
throw new IllegalArgumentException("Invalid password stored in DB");
}

String hashOfRequestPassword = hash(password, Base64.decodeBase64(saltAndPassword[1]), hashMethod);
return hashOfRequestPassword.equals(new String(saltAndPassword[0]));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
Expand All @@ -26,6 +28,10 @@ public class SaturnConsoleUtils {

private static Random random = new Random();

public static Logger getAuditLogger() {
return LoggerFactory.getLogger("AUDITLOG");
}

public static String parseMillisecond2DisplayTime(String longInStr) {
return parseMillisecond2DisplayTime(longInStr, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.vip.saturn.job.console.service.impl;

import com.vip.saturn.job.console.exception.SaturnJobConsoleException;
import com.vip.saturn.job.console.mybatis.entity.User;
import com.vip.saturn.job.console.mybatis.repository.UserRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.junit.Assert.*;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class AuthenticationServiceImplTest {

@Mock
private UserRepository userRepository;

@InjectMocks
private AuthenticationServiceImpl authnService;

@Test
public void testAuthenticateSuccessfully() throws SaturnJobConsoleException {
authnService.setHashMethod("plaintext");
User user = createUser("jeff", "password");
when(userRepository.select("jeff")).thenReturn(user);

assertEquals(user, authnService.authenticate("jeff", "password"));
}

@Test
public void testAuthenticationFailWhenUserIsNotFound() throws SaturnJobConsoleException {
authnService.setHashMethod("plaintext");
when(userRepository.select("john")).thenReturn(null);

assertNull(authnService.authenticate("john", "password"));
}

@Test
public void testAuthenticationFailWhenPasswordInputIsEmpty() throws SaturnJobConsoleException {
authnService.setHashMethod("plaintext");
assertNull(authnService.authenticate("john", ""));
}

private User createUser(String username, String password) {
User user = new User();
user.setUserName(username);
user.setPassword(password);

return user;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.vip.saturn.job.console.utils;

import org.junit.Test;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import static org.junit.Assert.*;

public class PasswordUtilsTest {

@Test
public void testGenSaltedPassword() throws Exception {
String password = PasswordUtils.genPassword("password", "salt".getBytes(), "PBKDF2WithHmacSHA1");
assertEquals("osJkYYaChHS3VFkaVHwY8TLYjXRMFSZVpHAWGhoFITU=$c2FsdA==", password);
}

@Test
public void testValidate() throws Exception {
assertTrue(PasswordUtils.validate("password", "osJkYYaChHS3VFkaVHwY8TLYjXRMFSZVpHAWGhoFITU=$c2FsdA==", "PBKDF2WithHmacSHA1"));
assertFalse(PasswordUtils.validate("password1", "osJkYYaChHS3VFkaVHwY8TLYjXRMFSZVpHAWGhoFITU=$c2FsdA==", "PBKDF2WithHmacSHA1"));
assertTrue(PasswordUtils.validate("password", "password", "plaintext"));
assertFalse(PasswordUtils.validate("password1", "password", "plaintext"));
}

@Test
public void testValidateWherePasswordInDBisMalfomred() {
int count = 0;
try {
PasswordUtils.validate("password", "password", "PBKDF2WithHmacSHA1");
} catch (Exception e) {
count++;
assertEquals("Invalid password stored in DB", e.getMessage());
}

assertEquals(1, count);
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
package com.vip.saturn.job.console.springboot;

import com.vip.saturn.job.console.filter.AuthenticationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.CharacterEncodingFilter;

import com.vip.saturn.job.console.filter.RecordLastVisit;

@Component
public class SaturnFilterRegister {

@Bean
public FilterRegistrationBean registerEncodingFilter() {
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceEncoding(true);
FilterRegistrationBean registration = new FilterRegistrationBean(encodingFilter);
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
@Value("${authentication.enabled:false}")
private boolean authenticationEnabled;

@Bean
public FilterRegistrationBean registerLastVisitFilter() {
RecordLastVisit lastVisit = new RecordLastVisit();
FilterRegistrationBean registration = new FilterRegistrationBean(lastVisit);
registration.addUrlPatterns("/*");
registration.setOrder(3);
public FilterRegistrationBean registerAuthenticationFilter() {
AuthenticationFilter filter = new AuthenticationFilter();
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(authenticationEnabled);
registration.addUrlPatterns("/console/*");
registration.setOrder(0);
return registration;
}


}
1 change: 1 addition & 0 deletions saturn-console/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ spring.main.show-banner=true

console.version=saturn-dev
authorization.enabled.default=false
authentication.enabled=false
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
class="com.vip.saturn.job.console.service.impl.statistics.StatisticsRefreshServiceImpl"/>
<bean id="registryCenterService"
class="com.vip.saturn.job.console.service.impl.RegistryCenterServiceImpl"/>
<!-- Service Beans End -->
<bean id="authenticationService"
class="com.vip.saturn.job.console.service.impl.AuthenticationServiceImpl"/>
<!-- Service Beans End -->

<aop:aspectj-autoproxy/>

Expand Down
Loading

0 comments on commit 786f58a

Please sign in to comment.