Custom image verification and verification code verification
Spring Security principle
Green: Check whether the request contains this information
Blue: Exception handling
Orange: Determines whether the request can access the service
Custom Login
The original Spring Security login method is not applicable in a project with a separate front and back end, so we need to customize the login method.
Custom validates successful processor
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private UserMapper userMapper;
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// After a successful login, you may need to return to the foreground what menu permissions the current user has, issue tokens, change database data, etc.
// Then the foreground dynamic control menu display, specific according to their own business needs to expand
// Return json data
CommonReturnType result = CommonReturnType.success("Login successful");
httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code
Custom validation failure handler
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// Return json data
CommonReturnType result = null;
if (e instanceof AccountExpiredException) {
// The account has expired
result = CommonReturnType.fail("Account expired");
} else if (e instanceof InternalAuthenticationServiceException) {
// The password is incorrect
result = CommonReturnType.fail("User does not exist");
} else if(e instanceof BadCredentialsException) {
// User does not exist
result = CommonReturnType.fail(e.getMessage());
} else if (e instanceof CredentialsExpiredException) {
// Password expired
result = CommonReturnType.fail("Password expired");
} else if (e instanceof DisabledException) {
// The account is unavailable
result = CommonReturnType.fail("Account is disabled.");
} else if (e instanceof LockedException) {
// The account is locked
result = CommonReturnType.fail("Account lock");
} else if(e instanceof NonceExpiredException) {
// Remote login
result = CommonReturnType.fail("Remote Login");
} else if(e instanceof SessionAuthenticationException) {
/ / the session
result = CommonReturnType.fail("The session error");
} else if(e instanceof ValidateCodeException) {
// The verification code is abnormal
result = CommonReturnType.fail(e.getMessage());
} else {
// Other unknown exceptions
result = CommonReturnType.fail(e.getMessage());
}
httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code
Anonymous access (unlogged access) to the handler
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
CommonReturnType result = CommonReturnType.fail("Login required to access the service");
httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code
Access denied handler
@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
CommonReturnType result = CommonReturnType.fail("Access to services requires administrator identity");
httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code
Logout successful processor
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
CommonReturnType result = CommonReturnType.success("Logout successful.");
httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code
Security configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// The database queries the user service
@Autowired
private UserNameDetailService userdetailservice;
// Do not log in to the processor (anonymous access no permission processing)
@Autowired
private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;
// Session expiration policy handler (remote login)
@Autowired
private CustomizeSessionInformationExpiredStrategy customizeSessionInformationExpiredStrategy;
// Log in to the processor successfully
@Autowired
private CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;
// Failed to log in to the handler
@Autowired
private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;
// The handler is denied permission
@Autowired
private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
// Log out the successful handler
@Autowired
private CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;
// Image captcha filter
@Autowired
private ValidateImageCodeFilter validateImageCodeFilter;
// SMS verification code filter
@Autowired
private SmsFilter smsFilter;
// Configure the SMS verification code
@Autowired
private SmsAuthenticationConfig smsAuthenticationConfig;
/** * Custom database search authentication *@param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userdetailservice).passwordEncoder(passwordEncoder());
}
/** * Set encryption mode *@return* /
@Bean
public BCryptPasswordEncoder passwordEncoder(a) {
return new BCryptPasswordEncoder();
}
/** * Configure login *@param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// Enable cross-domain and disable protection
http.csrf().disable().cors();
// Register a custom image captcha filter
http.addFilterBefore(validateImageCodeFilter, UsernamePasswordAuthenticationFilter.class);
// SMS validator
http.addFilterBefore(smsFilter, ValidateImageCodeFilter.class);
// Configure SMS verification code authentication to Spring Security
http.apply(smsAuthenticationConfig);
// Change the default jump when no login or login expired
http.exceptionHandling().authenticationEntryPoint(customizeAuthenticationEntryPoint);
// Path permission
http.authorizeRequests()
.antMatchers("/api/v1/user/login"."/doc.html"
,"/aip/v1/qrs/cc"."/api/v1/user/mobile"
,"/api/v1/user/sms"."/api/v1/user/image")
.permitAll()
.antMatchers("/usr/add").hasAnyAuthority("admin")
.anyRequest().authenticated();
// Log out
http.logout()
.logoutUrl("/logout").logoutSuccessUrl("/test/hello").deleteCookies("JSESSIONID")
.logoutSuccessHandler(customizeLogoutSuccessHandler) // Logout successful logic processing
.and()
.formLogin()
.successHandler(customizeAuthenticationSuccessHandler) // Logon success logic processing
.failureHandler(customizeAuthenticationFailureHandler) // Logon failure logic processing
.and()
.exceptionHandling()
.accessDeniedHandler(customizeAccessDeniedHandler) // Logical processing of permission denial
.authenticationEntryPoint(customizeAuthenticationEntryPoint) // Anonymous Access No permission to access resources
// Session management
.and()
.sessionManagement()
.maximumSessions(1) // Maximum number of logins for the same user
.expiredSessionStrategy(customizeSessionInformationExpiredStrategy); // Remote login (session failure) processing logic
}
public SmsAuthenticationConfig getSmsAuthenticationConfig(a) {
returnsmsAuthenticationConfig; }}Copy the code
Querying user services in the database
@Service("userdetailservice")
public class UserNameDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleRelationService userRoleRelationService;
@Autowired
private RolePermissionRelationService rolePermissionRelationService;
@Autowired
private SysPermissionService sysPermissionService;
@SneakyThrows
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(null == username|| "".equals(username)) {
throw new UsernameNotFoundException("Username cannot be empty.");
}
// Query the user
// Look for user permissions
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(s1);
return new User(user.getUsername(), newBCryptPasswordEncoder().encode(user.getPassword()), user.getEnabled(),user.getAccountNotExpired(),user.getCredentialsNotExpired(),user.getAccountNotLocked(),auths); }}Copy the code
Custom image verification code
Principle:
First of all, we through an interface for image authentication code, at the same time to the server to save image authentication code, and then we add filter to the authentication code in front of the UsernamePasswordAuthenticationFilter validation
Image verification code
public class ImageCode implements Serializable {
// Image verification code
private BufferedImage image;
/ / verification code
private String code;
// Expiration time
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpire(a) {
return LocalDateTime.now().isAfter(expireTime);
}
public BufferedImage getImage(a) {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public String getCode(a) {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime(a) {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime; }}Copy the code
Custom validation exception
You need to throw a defined exception during the check
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = 5022575393500654458L;
public ValidateCodeException(String message) {
super(message); }}Copy the code
Randomly generate verification codes
public class ImageCodeUtil {
/** * Create image verification code *@return* /
public static ImageCode createImageCode(a) {
int width = 100; // Captcha image width
int height = 36; // Captcha image length
int length = 4; // Verification code bits
int expireIn = 120; // The validity period of the verification code is 120s
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200.250));
g.fillRect(0.0,width,height);
g.setFont(new Font("Times New Roman",Font.ITALIC, 35));
g.setColor(getRandColor(160.200));
for(int i = 0; i< 155; i++){
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
StringBuilder sRand = new StringBuilder();
String rand = null;
for(int i = 0; i<length; i++){
int anInt = random.nextInt(57);
if(anInt >= 10) {
if(anInt + 65> =91 && anInt + 65< =96) {
anInt += 6;
}
char ch = (char) (anInt + 65);
rand = String.valueOf(ch);
} else {
rand = String.valueOf(anInt);
}
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 15 * i + 15.28);
}
g.dispose();
return new ImageCode(image, sRand.toString(),expireIn);
}
private static Color getRandColor(int fc, int bc) {
Random random = new Random();
if(fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return newColor(r, g, b); }}Copy the code
Image verification code filter
Inherit to the OncePerRequestFilter and BasicAuthenticationFilter here is the same, because BasicAuthenticationFilter also inherited OncePerRequestFilter.
@Slf4j
@Component
public class ValidateImageCodeFilter extends OncePerRequestFilter {
@Autowired
// Custom validation failure handler
private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;
/ / here I choose the authentication code in HttpSessionSessionStrategy redis (often used for storage)
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Whether the request path contains the keyword login && The request sent must be POST
if (StringUtils.contains(request.getRequestURI(), "login")
&& StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
try {
// Start validation
validateCode(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
// If validation fails, a custom validation handler is used
customizeAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
// Verify the implementation
private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
// Take the verification code from SessionStrategy
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
// Pull the verification code from the request path
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
// The verification code is null
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("Captcha cannot be empty.");
}
// Verification code issuer verification
if (codeInSession == null) {
throw new ValidateCodeException("The captcha does not exist!");
}
// Whether the verification code is expired
if (codeInSession.isExpire()) {
sessionStrategy.removeAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
throw new ValidateCodeException("The verification code has expired!");
}
// The verification code is correct
if(! StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) {throw new ValidateCodeException("The captcha is not correct!");
}
// Remove the server verification code store
sessionStrategy.removeAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE"); }}Copy the code
Access to check the picture certificate code interface
@RequestMapping("/image")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = ImageCodeUtil.createImageCode();
ImageCode codeInRedis = new ImageCode(null,imageCode.getCode(),imageCode.getExpireTime());
new HttpSessionSessionStrategy().setAttribute(new ServletWebRequest(request), "SESSION_KEY_IMAGE_CODE", codeInRedis);
response.setContentType("image/jpeg; charset=utf-8");
response.setStatus(HttpStatus.OK.value());
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
Copy the code
Customize SMS verification codes
Different from the image verification code, the SMS verification code is a login method, while the image verification code is a parameter for login.
Here, we need to define a new method of login authentication. Let’s use the authentication method of account password to write.
SMS verification code filter
Intercepts the SMS verification code login request, forms a verification token, and then performs authentication. Finally, register the entire process with Spring Security
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String MOBILE_KEY = "mobile";
private String mobileParameter = MOBILE_KEY;
private boolean postOnly = true;
public SmsAuthenticationFilter(a) {
super(new AntPathRequestMatcher("/api/v1/user/mobile"."POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if(postOnly && ! request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
// Generate an authentication token, but it is not authenticated
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter(a) {
returnmobileParameter; }}Copy the code
SmsAuthenticationToken
In the interceptor in the previous step, we intercepted the SMS captcha login request, and we need to assemble an AuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public SmsAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials(a) {
return null;
}
@Override
public Object getPrincipal(a) {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials(a) {
super.eraseCredentials(); }}Copy the code
SmsAuthenticationProvider
Verify the validation token assembled above.
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Autowired
private MobileDetailService mobileDetailService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
UserDetails userDetails = mobileDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (null == userDetails) {
throw new InternalAuthenticationServiceException("No user corresponding to this phone number was found");
}
// Mark the validation result as validated
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class
aClass) {
return SmsAuthenticationToken.class.isAssignableFrom(aClass);
}
public UserDetailsService getUserDetailService(a) {
return mobileDetailService;
}
public void setUserDetailService(MobileDetailService mobileDetailService) {
this.mobileDetailService = mobileDetailService; }}Copy the code
Configure the SMS verification code process to Spring Security
@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private MobileDetailService mobileDetailService;
@Override
public void configure(HttpSecurity http) {
// A validation interceptor
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
// Set a manager for the validation interceptor
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// Set the handler whose validation was successful
smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// Set the handler whose validation failed
smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// A validation provider implements validation (adding permissions, etc.)
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
// Give the provider my login account information to get the service
smsAuthenticationProvider.setUserDetailService(mobileDetailService);
// Add this validator to the end of the user name loginhttp.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
Interface for obtaining verification codes
Direct return through the interface instead of SMS service, here still uses the sessionStrategy storage, according to the need to use redis and other third-party database access.
@RequestMapping("/sms")
public void createSms(HttpServletRequest request,HttpServletResponse response,String mobile) throws IOException {
SmsCode smsCode = RandomSmsUtil.createSMSCode();
new HttpSessionSessionStrategy().setAttribute(new ServletWebRequest(request),"SESSION_KEY_SMS_CODE" + mobile,smsCode);
response.getWriter().write(smsCode.getCode());
System.out.println("Your verification code is:" + smsCode.getCode() + "The valid time is:" + smsCode.getExpireTime());
}
Copy the code
Finally, remember that both of these customizations need to be registered in the configuration class. I’ve already configured this in advance when I configured autologon up front.
So far, the whole custom SMS verification code login, as well as the picture verification code, has been completed! This is sufficient for most login scenarios. If there is any mistake, please correct me!