Personal blog :www.zhenganwen.top, surprise at the end of the article! This article is a follow-up to the article “Developing Enterprise-level Authentication and Authorization in The Spring Security Technology Stack.
Use Spring Security to develop form-based authentication
Realize graphic verification code function
Function implementation
Since graphic captcha is a common feature, we write the logic in security-code
First, encapsulate the graph, the captcha in the graph, and the captcha expiration time
package top.zhenganwen.security.core.verifycode.dto;
import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/ * * *@author zhenganwen
* @date 2019/8/24
* @desc ImageCode
*/
@Data
public class ImageCode {
private String code;
private BufferedImage image;
// Verification code expiration time
private LocalDateTime expireTime;
public ImageCode(String code, BufferedImage image, LocalDateTime expireTime) {
this.code = code;
this.image = image;
this.expireTime = expireTime;
}
public ImageCode(String code, BufferedImage image, int durationSeconds) {
this(code, image, LocalDateTime.now().plusSeconds(durationSeconds));
}
public boolean isExpired(a) {
returnLocalDateTime.now().isAfter(expireTime); }}Copy the code
It then provides an interface to generate captcha
package top.zhenganwen.security.core.verifycode;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/ * * *@author zhenganwen
* @date 2019/8/24
* @desc VerifyCodeController
*/
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
/** * 1. Generate graphic verification code * 2. Save the verification code to Session * 3. Gives the graph response to the front end */
@GetMapping("/image")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = generateImageCode(67.23.4);
// Session read/write tool class, the first parameter is fixed
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode.getCode());
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
/ * * *@paramWidth Graphic width *@paramHeight Graph height *@paramStrLength Indicates the number of verification code characters *@return* /
private ImageCode generateImageCode(int width, int height, int strLength) {
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, 20));
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);
}
String sRand = "";
for (int i = 0; i < strLength; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6.16);
}
g.dispose();
return new ImageCode(sRand, image, 60);
}
/** * generates a random background stripe **@param fc
* @param bc
* @return* /
private 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
Release the interface for generating captcha in the security-Browser configuration class:
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/auth/require")
.loginProcessingUrl("/auth/login")
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers(
"/auth/require",
securityProperties.getBrowser().getLoginPage(),
"/verifyCode/image").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
Copy the code
To test the generation of the verification code in security-demo, add the verification code input box in login. HTML:
<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image" alt="">
<button type="submit">submit</button>
</form>
Copy the code
To access /login.html, the verification code is generated as follows:
Next we write the verification logic. Since Security does not provide a filter for verification, we need to define one and insert it before UsernamePasswordFilter:
package top.zhenganwen.security.core.verifycode;
import org.springframework.security.core.AuthenticationException;
/ * * *@author zhenganwen
* @date 2019/8/24
* @desc VerifyCodeException
*/
public class VerifyCodeException extends AuthenticationException {
public VerifyCodeException(String explanation) {
super(explanation); }}Copy the code
package top.zhenganwen.security.core.verifycode;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/ * * *@author zhenganwen
* @date 2019/8/24
* @desc VerifyCodeAuthenticationFilter
*/
@Component
// Inherits OncePerRequestFilter filters will only be executed once in a request
public class VerifyCodeAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private AuthenticationFailureHandler customAuthenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,
IOException {
// If it is a login request
if (Objects.equals(request.getRequestURI(), "/auth/login") && StringUtils.endsWithIgnoreCase(request.getMethod(), "POST")) {
try {
this.validateVerifyCode(new ServletWebRequest(request));
} catch (VerifyCodeException e) {
/ / if an exception is the use custom authentication failure processing, otherwise no one captured (because the filter is in front of the UsernamePasswordAuthenticationFilter)
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
}
}
filterChain.doFilter(request, response);
}
// Read the verification code from Session and compare it with the verification code submitted by the user
private void validateVerifyCode(ServletWebRequest request) {
String verifyCode = (String) request.getParameter("verifyCode");
if (StringUtils.isBlank(verifyCode)) {
throw new VerifyCodeException("Verification code cannot be empty.");
}
ImageCode imageCode = (ImageCode) sessionStrategy.getAttribute(request, VerifyCodeController.SESSION_KEY);
if (imageCode == null) {
throw new VerifyCodeException("Captcha does not exist");
}
if (imageCode.isExpired()) {
throw new VerifyCodeException("Verification code has expired. Please refresh the page.");
}
if (StringUtils.equals(verifyCode,imageCode.getCode()) == false) {
throw new VerifyCodeException("Verification code error");
}
// If the login succeeds, remove the verification code saved in the SessionsessionStrategy.removeAttribute(request, VerifyCodeController.SESSION_KEY); }}Copy the code
security-browser
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/auth/require")
.loginProcessingUrl("/auth/login")
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers(
"/auth/require",
securityProperties.getBrowser().getLoginPage(),
"/verifyCode/image").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
Copy the code
Go to /login. HTML and login directly without filling in anything, and return JSON as follows
{"cause":null."stackTrace": [...]. ."localizedMessage":"Verification code cannot be empty."."message":"Verification code cannot be empty."."suppressed": []} {"cause":null."stackTrace": [...]. ."localizedMessage":"Bad papers."."message":"Bad papers."."suppressed": []}Copy the code
Find a JSON string that returns two exceptions, one before or after (the two strings are attached, without any symbols in the middle), This is because we call in VerifyCodeAuthenticationFilter customAuthenticationFailureHandler authentication failure after processing, and then execute the doFilter, Then UsernamePasswordAuthenticationFilter will intercept the login request/auth/login, in the process of checking captured BadCredentialsException, Call customAuthenticationFailureHandler return another exceptionJSON string
There are two things that need to be optimized
-
The exception message returned should not contain a stack
Return in CustomAuthenticationFailureHandler exception information extracted from the exception, rather than direct return to the exception
// response.getWriter().write(objectMapper.writeValueAsString(exception)); response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponseResult(exception.getMessage()))); Copy the code
- in ` VerifyCodeAuthenticationFilter ` found authentication failed abnormal failure after processing, and call the certification should be `returnNow, there is no need to go through the following filter Javaif (Objects.equals(request.getRequestURI(), "/auth/login") && StringUtils.endsWithIgnoreCase(request.getMethod(), "POST")) { try { this.validateVerifyCode(new ServletWebRequest(request)); } catch (VerifyCodeException e) {// If an exception is thrown, use a custom authentication failure handler. Otherwise no one captured (because the filter is in front of the UsernamePasswordAuthenticationFilter) customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);return;
}
}
filterChain.doFilter(request, response);
Copy the code
To test
{content: "Captcha cannot be empty"}Copy the code
Then test the verification code, fill in admin,123456 and graphics verification code after login, login success, Authentication success processor return Authentication
{
authorities: [
{
authority: "admin"
},
{
authority: "user"
}
],
details: {
remoteAddress: "0:0:0:0:0:0:0:1",
sessionId: "452F44596C9D9FF55DBA91A1F24E05B0"
},
authenticated: true,
principal: {
password: null,
username: "admin",
authorities: [
{
authority: "admin"
},
{
authority: "user"
}
],
accountNonExpired: true,
accountNonLocked: true,
credentialsNonExpired: true,
enabled: true
},
credentials: null,
name: "admin"
}
Copy the code
Reconstruct the graphics captcha function
At this point, the function of the graphical verification code we have finished basic implementation, but as we should not be content with the senior engineers, in the realization of function and should think about how to refactor your code to make the reusable function, while others require a different size, the number of different character, different authentication logic, can also reuse the code
The basic parameters of the graphic verification code are configurable
For example, the length and width of the graph, the number of characters of the verification code, and the duration of the validity period of the verification code
The effective mechanism of the general system configuration is as follows. As a dependent module, we need to provide a common default configuration, and the dependent application can add configuration items to cover the default configuration. Finally, when the application is running, it can dynamically switch the configuration by attaching parameters in the request
Security-core adds a configuration class
package top.zhenganwen.security.core.properties;
import lombok.Data;
/ * * *@author zhenganwen
* @date 2019/8/25
* @desc ImageCodeProperties
*/
@Data
public class ImageCodeProperties {
private int width=67;
private int height=23;
private int strLength=4;
private int durationSeconds = 60;
}
Copy the code
package top.zhenganwen.security.core.properties;
import lombok.Data;
/ * * *@author zhenganwen
* @date 2019/8/25
* @descVerifyCodeProperties encapsulates both graphic and SMS verification codes */
@Data
public class VerifyCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
}
Copy the code
package top.zhenganwen.security.core.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/ * * *@author zhenganwen
* @date 2019/8/23
* @descSecurityProperties encapsulates the configuration items */ for each module of the entire project
@Data
@ConfigurationProperties(prefix = "demo.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private VerifyCodeProperties code = new VerifyCodeProperties();
}
Copy the code
In the generate validation interface, change the corresponding parameters to dynamic reading
package top.zhenganwen.security.core.verifycode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/ * * *@author zhenganwen
* @date 2019/8/24
* @desc VerifyCodeController
*/
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
@Autowired
private SecurityProperties securityProperties;
/** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end */
@GetMapping("/image")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// First read width/height in the URL argument, if not in the configuration file
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
ImageCode imageCode = generateImageCode(width, height, securityProperties.getCode().getImage().getStrLength());
// Session read/write tool class, the first parameter is fixed
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
/ * * *@paramWidth Graphic width *@paramHeight Graph height *@paramStrLength Indicates the number of verification code characters *@return* /
private ImageCode generateImageCode(int width, int height, int strLength) {
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, 20));
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);
}
String sRand = "";
for (int i = 0; i < strLength; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6.16);
}
g.dispose();
return newImageCode(sRand, image, securityProperties.getCode().getImage().getDurationSeconds()); }}Copy the code
To test the application level configuration of the verification code to override the default number of characters, add the configuration item in the security-demo application.properties file
demo.security.code.image.strLength=6
Copy the code
The test request parameter-level configuration overrides the application level configuration
demo.security.code.image.width=100
Copy the code
Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=200" alt="">
Copy the code
/login. HTML, the width of the graph is 200, the number of captcha characters is 6, and the test is successful
Verification code Specifies the interface intercepted by the authentication filter
Currently, our VerifyCodeFilter only intercepts login requests and performs verification. Other interfaces may also require verification (perhaps for illegal duplicate requests), so we need to support applications that can dynamically configure interfaces that require verification, for example
demo.security.code.image.url=/user,/user/*
Copy the code
Indicates that the request /user and /user/* require verification code
So we add a property that can be configured to intercept urIs
@Data
public class ImageCodeProperties {
private int width=67;
private int height=23;
private int strLength=4;
private int durationSeconds = 60;
// A list of URIs to intercept, separated by commas
private String uriPatterns;
}
Copy the code
Then in VerifyCodeAuthenticationFilter demo. In the configuration file is read. The security code. The image. The uriPatterns and initialize a uriPatternSet collection, In the interception logic, the collection is traversed and the intercepted URI is patternmatched with the elements of the collection. If there is a match, it means that the URI needs to check the verification code. If the verification fails, an exception is thrown and left to the authentication failure processor
@Component
public class VerifyCodeAuthenticationFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler customAuthenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
private Set<String> uriPatternSet = new HashSet<>();
// uri matching tool class, help us to do similar /user/1 to /user/* matching
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet(a) throws ServletException {
super.afterPropertiesSet();
String uriPatterns = securityProperties.getCode().getImage().getUriPatterns();
if (StringUtils.isNotBlank(uriPatterns)) {
String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(uriPatterns, ",");
uriPatternSet.addAll(Arrays.asList(strings));
}
uriPatternSet.add("/auth/login");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,
IOException {
for (String uriPattern : uriPatternSet) {
if (antPathMatcher.match(uriPattern, request.getRequestURI())) {
try {
this.validateVerifyCode(new ServletWebRequest(request));
} catch (VerifyCodeException e) {
/ / if an exception is thrown, the use of custom authentication failure processing, otherwise no one captured (because the filter match in front of the UsernamePasswordAuthenticationFilter) is thrown to the front
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
break;
}
}
filterChain.doFilter(request, response);
}
private void validateVerifyCode(ServletWebRequest request) {...}
}
Copy the code
We write the initialization logic for uriPatternSet in the afterPropertiesSet method of the InitializingBean interface, which is equivalent to configuring an init-method tag in traditional Spring.xml, This method can be in all the autowire VerifyCodeAuthenticationFilter properties are performed by the spring after the assignment
Modify the configuration item to uriPattern=/user/* If the user accesses /user and /user/1, the verification code cannot be empty. If the user logs in to /login. HTML and accesses /user after the restart, the verification code cannot be empty
Graphical captcha generation logic is configurable — incrementally adapted to change
Now our graphic captcha style is fixed, can only generate a number of captcha, others want to change a style or generate letters, man captcha seems powerless. He wondered if he could implement an interface that returned custom ImageCode to use his own captcha generation logic as he did with Spring
Spring provides the idea that you can implement an interface instead of a Spring implementation. A common design pattern is that you don’t need to change the code when you need to extend functionality, but simply add an implementation class to accommodate the change incrementally
First we abstract the logic that generates the graphic captcha into an interface
package top.zhenganwen.security.core.verifycode;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
/ * * *@author zhenganwen
* @date 2019/8/25
* @descImageCodeGenerator Graphic verification code generator interface */
public interface ImageCodeGenerator {
ImageCode generateImageCode(int width, int height, int strLength, int durationSeconds);
}
Copy the code
The method of generating graphical captcha, previously written in Controller, is then used as the default implementation of this interface
package top.zhenganwen.security.core.verifycode;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/ * * *@author zhenganwen
* @date 2019/8/25
* @desc DefaultImageCodeGenerator
*/
public class DefaultImageCodeGenerator implements ImageCodeGenerator {
@Override
public ImageCode generateImageCode(int width, int height, int strLength, int durationSeconds) {
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, 20));
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);
}
String sRand = "";
for (int i = 0; i < strLength; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6.16);
}
g.dispose();
return new ImageCode(sRand, image, durationSeconds);
}
/** * generates a random background stripe **@param fc
* @param bc
* @return* /
private 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
ConditionOnMissingBean (@conditiononMissingBean). ConditionOnMissingBean (@conditiononMissingBean); ConditionOnMissingBean (@conditiononMissingBean); Determine if there is a bean with the ID imageCodeGenerator in the container. If not, instantiate it and use it as a bean with the ID imageCodeGenerator
package top.zhenganwen.security.core;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.DefaultImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.ImageCodeGenerator;
/ * * *@author zhenganwen
* @date 2019/8/23
* @desc SecurityCoreConfig
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ImageCodeGenerator imageCodeGenerator(a) {
ImageCodeGenerator imageCodeGenerator = new DefaultImageCodeGenerator();
returnimageCodeGenerator; }}Copy the code
The captcha generation interface was changed to rely on the Captcha generator interface to generate captcha (abstract oriented programming to accommodate changes) :
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ImageCodeGenerator imageCodeGenerator;
/** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end */
@GetMapping("/image")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// First read width/height in the URL argument, if not in the configuration file
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
ImageCode imageCode = imageCodeGenerator.generateImageCode(width, height,
securityProperties.getCode().getImage().getStrLength(),
securityProperties.getCode().getImage().getDurationSeconds());
// Session read/write tool class, the first parameter is fixed
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream()); }}Copy the code
Restart the service and log in to ensure that the refactoring has not changed the functionality of the code
Finally, we added a custom graphic captcha generator in security-Demo to replace the default:
package top.zhenganwen.securitydemo.security;
import top.zhenganwen.security.core.verifycode.ImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
/ * * *@author zhenganwen
* @date 2019/8/25
* @desc CustomImageCodeGenerator
*/
@Component("imageCodeGenerator")
public class CustomImageCodeGenerator implements ImageCodeGenerator {
@Override
public ImageCode generateImageCode(int width, int height, int strLength, int durationSeconds) {
System.out.println("Call a custom code generator");
return null; }}Copy the code
Here we simply print the log and return a NULL so that login. HTML will throw an exception if it uses our custom graphic captcha generator interface to generate a graphic captcha. Note that the value attribute of @Component must match the name attribute of @conditiononmissingBean for this substitution to work
Implement remember me function
demand
Sometimes users want to check a “Remember me” box when filling out a login form to access protected urls without logging in for a certain period of time after login
implementation
In this section, we will implement the following function:
-
First of all, the page needs a “Remember me” box, whose name attribute should be remembered -me (configurable) and value attribute should be true
<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=200" alt=""> <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button> </form> Copy the code
-
Create a table persistent_logins in the database corresponding to the data source, and the table creation statement is in the CREATE_TABLE_SQL variable of JdbcTokenRepositoryImpl
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key."+"token varchar(64) not null, last_used timestamp not null) Copy the code
-
Add “rememberMe” to the seurity configuration class. Here, since cookies are restricted to the browser, we configure them in the security-browser module, below rememberMe() section
@Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Bean public PersistentTokenRepository persistentTokenRepository(a) { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/auth/require") .loginProcessingUrl("/auth/login") .successHandler(customAuthenticationSuccessHandler) .failureHandler(customAuthenticationFailureHandler) .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(3600) .userDetailsService(userDetailsService) // You can configure the name attribute of the page selection box // .rememberMeParameter() .and() .authorizeRequests() .antMatchers( "/auth/require", securityProperties.getBrowser().getLoginPage(), "/verifyCode/image").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); } Copy the code
-
test
The persistent_logins database table is displayed. The persistent_logins database table is displayed. The persistent_logins database table is displayed. Closing the service simulates closing the Session (since the Session is the saving server, closing the server is a better guarantee of closing the Session than closing the browser). After the service is restarted, protected /user cannot be accessed directly
Source code analysis
Above is the “remember me”, the user first login sequence diagram, check in AbstractAuthenticationProcessingFilter user name password after successful at the end of the method will be called successfulAuthentication, Check the source code (partially omitted) :
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
Copy the code
Found in successHandler. OnAuthenticationSuccess () call authentication processor before success, also performs the rememberMeServices. LoginSuccess, This method is used to the database insert a username – token of modules and the token Cookie, specific logic in PersistentTokenBasedRememberMeServices# onLoginSuccess ()
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
}catch (Exception e) {
logger.error("Failed to save persistent token ", e); }}Copy the code
During our set tokenValiditySeconds, if the user login but not from the same browser to access the protected services, RememberMeAuthenticationFilter will intercept to the request:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) { Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); . }Copy the code
AutoLogin () is called to try to read the token from the Cookie and query the username-token from the persistence layer. If found, UserDetailsService is called to find the user based on username. Find the successful Authentication generated by the new Authentication and save it to the current thread safe:
AbstractRememberMeServices#autoLogin
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
returncreateSuccessfulAuthentication(request, user); }... }Copy the code
PersistentTokenBasedRememberMeServices
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
Copy the code
SMS verification code login
Before, we used the traditional login method of user name and password, but with the popularity of SMS verification code login and third-party applications such as QQ login, the traditional login method can no longer meet our needs
The user name and password authentication process is already solidified in the Security framework, we can only write some implementation interface extension details, and the general process is not able to change. Therefore, to achieve SMS verification code login, we need to define a set of login procedures
Interface for sending SMS verification codes
To realize the SMS verification code function, we need to provide this interface first. The front end can call this interface to transmit the SMS verification code to the mobile phone number. As follows, the verification code is sent by clicking the event on the login page of the browser. It should have been asynchronously called the sending interface through AJAX. Here, for the convenience of demonstration, hyperlink is used for synchronous invocation, and the mobile phone number is written down instead of dynamically obtaining the mobile phone number entered by the user through JS
<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=200" alt="">
<input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
<hr/>
<form action="/auth/sms" method="post">Mobile phone no. :<input type="text" name="phoneNumber" value="12345678912">Verification code:<input type="text"><a href="/verifyCode/sms? phoneNumber=12345678912">Click on send</a>
<input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
Copy the code
Refactoring PO
Security-core creates a new class to encapsulate the properties of the SMS verification code:
package top.zhenganwen.security.core.verifycode.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsCode {
protected String code;
protected LocalDateTime expireTime;
public boolean isExpired(a) {
returnLocalDateTime.now().isAfter(expireTime); }}Copy the code
Here, since the previous ImageCode also had these two properties, we rename SmsCode to VerifyCode for ImageCode to inherit to reuse code
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerifyCode {
protected String code;
protected LocalDateTime expireTime;
public boolean isExpired(a) {
returnLocalDateTime.now().isAfter(expireTime); }}Copy the code
@Data
public class ImageCode extends VerifyCode{
private BufferedImage image;
public ImageCode(String code, BufferedImage image, LocalDateTime expireTime) {
super(code,expireTime);
this.image = image;
}
public ImageCode(String code, BufferedImage image, int durationSeconds) {
this(code, image, LocalDateTime.now().plusSeconds(durationSeconds)); }}Copy the code
Refactor the captcha generator
Next we need a SHORT message captcha generator, not as complex as a graphic captcha generator. ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean
package top.zhenganwen.security.core.verifycode.generator;
import top.zhenganwen.security.core.verifycode.dto.VerifyCode;
public interface VerifyCodeGenerator<T extends VerifyCode> {
/** * Generates a verification code *@return* /
T generateVerifyCode(a);
}
Copy the code
package top.zhenganwen.security.core.verifycode.generator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
public class DefaultImageCodeGenerator implements VerifyCodeGenerator<ImageCode> {
@Autowired
private SecurityProperties securityProperties;
@Autowired
HttpServletRequest request;
@Override
public ImageCode generateVerifyCode(a) {
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
int strLength = securityProperties.getCode().getImage().getStrLength();
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, 20));
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);
}
String sRand = "";
for (int i = 0; i < strLength; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6.16);
}
g.dispose();
return newImageCode(sRand, image, securityProperties.getCode().getImage().getDurationSeconds()); }... }Copy the code
package top.zhenganwen.securitydemo.security;
import top.zhenganwen.security.core.verifycode.generator.VerifyCodeGenerator;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;
//@Component
public class CustomImageCodeGenerator implements VerifyCodeGenerator<ImageCode> {
@Override
public ImageCode generateVerifyCode(a) {
System.out.println("Call a custom code generator");
return null; }}Copy the code
package top.zhenganwen.security.core.verifycode.generator;
import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.dto.VerifyCode;
import java.time.LocalDateTime;
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements VerifyCodeGenerator<VerifyCode> {
@Autowired
private SecurityProperties securityProperties;
@Override
public VerifyCode generateVerifyCode(a) {
// Generate a random string of pure digits strLength
String randomCode = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getStrLength());
return newVerifyCode(randomCode, LocalDateTime.now().plusSeconds(securityProperties.getCode().getSms().getDurationSeconds())); }}Copy the code
SMS verification code sender
Generated after the message authentication code we need to save it in the Session and call the SMS service provider interface will be sent out, because the future rely on our application can configure different SMS service provider interface, in order to guarantee the scalability of the code we need to send a text message this behavior into abstract interface and provide a default can be covered, This allows applications that depend on us to enable their SMS logic by injecting a new implementation
package top.zhenganwen.security.core.verifycode;
public interface SmsCodeSender {
/** * Send SMS verification code * according to mobile phone number@param smsCode
* @param phoneNumber
*/
void send(String smsCode, String phoneNumber);
}
Copy the code
package top.zhenganwen.security.core.verifycode;
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String smsCode, String phoneNumber) {
// This is just a simple print, actually should call the SMS service provider to send SMS verification code to the mobile phone number
System.out.printf("Send SMS verification code %s to mobile phone number %s", phoneNumber, smsCode); }}Copy the code
package top.zhenganwen.security.core;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.DefaultImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.DefaultSmsCodeSender;
import top.zhenganwen.security.core.verifycode.ImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.SmsCodeSender;
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ImageCodeGenerator imageCodeGenerator(a) {
ImageCodeGenerator imageCodeGenerator = new DefaultImageCodeGenerator();
return imageCodeGenerator;
}
@Bean
@ConditionalOnMissingBean(name = "smsCodeSender")
public SmsCodeSender smsCodeSender(a) {
return newDefaultSmsCodeSender(); }}Copy the code
Refactoring configuration classes
package top.zhenganwen.security.core.properties;
import lombok.Data;
@Data
public class SmsCodeProperties {
// Number of SMS verification codes. The default value is 4
private int strLength = 4;
// Valid time, 60 seconds by default
private int durationSeconds = 60;
}
Copy the code
package top.zhenganwen.security.core.properties;
import lombok.Data;
@Data
public class ImageCodeProperties extends SmsCodeProperties{
private int width=67;
private int height=23;
private String uriPatterns;
public ImageCodeProperties(a) {
// The graphic verification code displays 6 characters by default
this.setStrLength(6);
// The graphical verification code expires in 3 minutes by default
this.setDurationSeconds(180); }}Copy the code
package top.zhenganwen.security.core.properties;
import lombok.Data;
@Data
public class VerifyCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
private SmsCodeProperties sms = new SmsCodeProperties();
}
Copy the code
Interface for sending SMS verification codes
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
public static final String IMAGE_CODE_SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
public static final String SMS_CODE_SESSION_KEY = "SESSION_KEY_SMS_CODE";
@Autowired
private VerifyCodeGenerator<ImageCode> imageCodeGenerator;
@Autowired
private VerifyCodeGenerator<VerifyCode> smsCodeGenerator;
@Autowired
private SmsCodeSender smsCodeSender;
/** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end */
@GetMapping("/image")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = imageCodeGenerator.generateVerifyCode();
sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CODE_SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
/** * 1. Generate the SMS verification code * 2. Save the verification code to the session * 3. Invokes the SMS verification code sender to send SMS */
@GetMapping("/sms")
public void smsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
long phoneNumber = ServletRequestUtils.getRequiredLongParameter(request, "phoneNumber");
VerifyCode verifyCode = smsCodeGenerator.generateVerifyCode();
sessionStrategy.setAttribute(newServletWebRequest(request), SMS_CODE_SESSION_KEY, verifyCode); smsCodeSender.send(verifyCode.getCode(), String.valueOf(phoneNumber)); }}Copy the code
test
In security-Browser, we will allow access to the new interface /verifyCode/ SMS:
.authorizeRequests()
.antMatchers(
"/auth/require",
securityProperties.getBrowser().getLoginPage(),
"/verifyCode/**").permitAll()
.anyRequest().authenticated()
Copy the code
Go to /login.html and click send hyperlink. The background output is as follows:
Send SMS verification code 1220 to the mobile phone number 12345678912Copy the code
Refactoring – Template methods & dependency lookup
Now our two methods in VerifyCodeController have the same main flow for imageCode and smsCode:
- Generate captcha
- Save the verification code, for example, to
Session
In the,redis
In, etc. - Send the verification code to the user
In this case, we can apply the template approach to the design pattern (see my other article, Graphic Design Patterns), and the reconstructed class diagram looks like this:
Constant class
public class VerifyCodeConstant {
public static final String IMAGE_CODE_SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
public static final String SMS_CODE_SESSION_KEY = "SESSION_KEY_SMS_CODE";
public static final String VERIFY_CODE_PROCESSOR_IMPL_SUFFIX = "CodeProcessorImpl";
public static final String VERIFY_CODE_Generator_IMPL_SUFFIX = "CodeGenerator";
public static final String PHONE_NUMBER_PARAMETER_NAME = "phoneNumber";
}
Copy the code
public enum VerifyCodeTypeEnum {
IMAGE("image"),SMS("sms");
private String type;
public String getType(a) {
return type;
}
VerifyCodeTypeEnum(String type) {
this.type = type; }}Copy the code
Captcha sending handler – template method & Interface isolation & dependency lookup
public interface VerifyCodeProcessor {
/** * Send verification code logic * 1. Generate verification code * 2. Save verification code * 3. Send the verification code *@paramRequest is a utility class that encapsulates request and Response so that we don't have to pass {@linkJavax.mail. Servlet. HTTP. It} and {@linkJavax.mail. Servlet. HTTP. HttpServletResponse} * /
void sendVerifyCode(ServletWebRequest request);
}
Copy the code
public abstract class AbstractVerifyCodeProcessor<T extends VerifyCode> implements VerifyCodeProcessor {
@Override
public void sendVerifyCode(ServletWebRequest request) {
T verifyCode = generateVerifyCode(request);
save(request, verifyCode);
send(request, verifyCode);
}
/** * Generates a verification code **@param request
* @return* /
public abstract T generateVerifyCode(ServletWebRequest request);
/** * Save the verification code **@param request
* @param verifyCode
*/
public abstract void save(ServletWebRequest request, T verifyCode);
/** ** Send verification code **@param request
* @param verifyCode
*/
public abstract void send(ServletWebRequest request, T verifyCode);
}
Copy the code
@Component
public class ImageCodeProcessorImpl extends AbstractVerifyCodeProcessor<ImageCode> {
private Logger logger = LoggerFactory.getLogger(getClass());
/** * Spring will look for all {@linkAn instance of VerifyCodeGenerator} is injected into the map as key=beanId,value=bean */
@Autowired
private Map<String, VerifyCodeGenerator> verifyCodeGeneratorMap = new HashMap<>();
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
public ImageCode generateVerifyCode(ServletWebRequest request) {
VerifyCodeGenerator<ImageCode> verifyCodeGenerator = verifyCodeGeneratorMap.get(IMAGE.getType() + VERIFY_CODE_Generator_IMPL_SUFFIX);
return verifyCodeGenerator.generateVerifyCode();
}
@Override
public void save(ServletWebRequest request, ImageCode imageCode) {
sessionStrategy.setAttribute(request,IMAGE_CODE_SESSION_KEY, imageCode);
}
@Override
public void send(ServletWebRequest request, ImageCode imageCode) {
HttpServletResponse response = request.getResponse();
try {
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
} catch (IOException e) {
logger.error("Output graphic verification code :{}", e.getMessage()); }}}Copy the code
@Component
public class SmsCodeProcessorImpl extends AbstractVerifyCodeProcessor<VerifyCode> {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private Map<String, VerifyCodeGenerator> verifyCodeGeneratorMap = new HashMap<>();
@Autowired
private SmsCodeSender smsCodeSender;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
public VerifyCode generateVerifyCode(ServletWebRequest request) {
VerifyCodeGenerator verifyCodeGenerator = verifyCodeGeneratorMap.get(SMS.getType() + VERIFY_CODE_Generator_IMPL_SUFFIX);
return verifyCodeGenerator.generateVerifyCode();
}
@Override
public void save(ServletWebRequest request, VerifyCode verifyCode) {
sessionStrategy.setAttribute(request, SMS_CODE_SESSION_KEY, verifyCode);
}
@Override
public void send(ServletWebRequest request, VerifyCode verifyCode) {
try {
long phoneNumber = ServletRequestUtils.getRequiredLongParameter(request.getRequest(),PHONE_NUMBER_PARAMETER_NAME);
smsCodeSender.send(verifyCode.getCode(),String.valueOf(phoneNumber));
} catch (ServletRequestBindingException e) {
throw new RuntimeException("Cell phone number cannot be empty."); }}}Copy the code
Captcha generator
public interface VerifyCodeGenerator<T extends VerifyCode> {
/** * Generates a verification code *@return* /
T generateVerifyCode(a);
}
Copy the code
public class DefaultImageCodeGenerator implements VerifyCodeGenerator<ImageCode> {
@Autowired
private SecurityProperties securityProperties;
@Autowired
HttpServletRequest request;
@Override
public ImageCode generateVerifyCode(a) {
int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
int strLength = securityProperties.getCode().getImage().getStrLength();
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, 20));
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);
}
String sRand = "";
for (int i = 0; i < strLength; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6.16);
}
g.dispose();
return new ImageCode(sRand, image, securityProperties.getCode().getImage().getDurationSeconds());
}
/** * generates a random background stripe **@param fc
* @param bc
* @return* /
private 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
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements VerifyCodeGenerator<VerifyCode> {
@Autowired
private SecurityProperties securityProperties;
@Override
public VerifyCode generateVerifyCode(a) {
// Generate a random string of pure digits strLength
String randomCode = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getStrLength());
return newVerifyCode(randomCode, LocalDateTime.now().plusSeconds(securityProperties.getCode().getSms().getDurationSeconds())); }}Copy the code
Verification code sender
public interface SmsCodeSender {
/** * Send SMS verification code * according to mobile phone number@param smsCode
* @param phoneNumber
*/
void send(String smsCode, String phoneNumber);
}
Copy the code
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String smsCode, String phoneNumber) {
System.out.printf("Send SMS verification code %s to mobile phone number %s", phoneNumber, smsCode); }}Copy the code
Verification code sending interface
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {
/* private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private VerifyCodeGenerator
imageCodeGenerator; @Autowired private VerifyCodeGenerator
smsCodeGenerator; @Autowired private SmsCodeSender smsCodeSender; * /
/** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end *//* @GetMapping("/image") public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = imageCodeGenerator.generateVerifyCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CODE_SESSION_KEY, imageCode); ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream()); } * //** * 1. Generate the SMS verification code * 2. Save the verification code to the session * 3. Invokes the SMS verification code sender to send SMS *//* @GetMapping("/sms") public void smsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException { long phoneNumber = ServletRequestUtils.getRequiredLongParameter(request, "phoneNumber"); VerifyCode verifyCode = smsCodeGenerator.generateVerifyCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, verifyCode); smsCodeSender.send(verifyCode.getCode(), String.valueOf(phoneNumber)); } * /
@Autowired
private Map<String, VerifyCodeProcessor> verifyCodeProcessorMap = new HashMap<>();
@GetMapping("/{type}")
public void sendVerifyCode(@PathVariable String type, HttpServletRequest request, HttpServletResponse response) {
if (Objects.equals(type, IMAGE.getType()) == false && Objects.equals(type, SMS.getType()) == false) {
throw new IllegalArgumentException("Unsupported captcha types");
}
VerifyCodeProcessor verifyCodeProcessor = verifyCodeProcessorMap.get(type + VERIFY_CODE_PROCESSOR_IMPL_SUFFIX);
verifyCodeProcessor.sendVerifyCode(newServletWebRequest(request, response)); }}Copy the code
The configuration class
package top.zhenganwen.security.core;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.generator.DefaultImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.sender.DefaultSmsCodeSender;
import top.zhenganwen.security.core.verifycode.generator.VerifyCodeGenerator;
import top.zhenganwen.security.core.verifycode.sender.SmsCodeSender;
/ * * *@author zhenganwen
* @date 2019/8/23
* @desc SecurityCoreConfig
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public VerifyCodeGenerator imageCodeGenerator(a) {
VerifyCodeGenerator imageCodeGenerator = new DefaultImageCodeGenerator();
return imageCodeGenerator;
}
@Bean
@ConditionalOnMissingBean(name = "smsCodeSender")
public SmsCodeSender smsCodeSender(a) {
return newDefaultSmsCodeSender(); }}Copy the code
test
Keep in mind that refactoring only improves the quality and readability of your code, so after each small refactoring, always test to see if the original functionality has been affected
-
Access /login. HTML to login with the user name and password, and then access the protected service /user
-
Visit /login. HTML and click Send to check whether the console prints send logs
-
Modify /login. HTML to set the width of the graphic verification code to 600
Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=600" alt=""> Copy the code
Test passed, refactoring successful!
SMS verification code login
To realize the SMS verification code login process, we can learn from the existing user name and password login process and analyze which components need to be realized by ourselves:
First we need an SmsAuthenticationFilter to intercept the SMS login request for Authentication, during which it will encapsulate the login information into an Authentication request AuthenticationManager for Authentication
AuthenticationManager will go through all authenticationProviders to find out which AuthenticationProvider supports Authentication and call Authenticate for the actual Authentication. So we need to implement your own Authentication (SmsAuthenticationToken) and the certification of the Authentication AuthenticationProvider (SmsAuthenticationProvider), And add SmsAuthenticationProvider SpringSecurty AuthenticationProvider collection, In order to make the AuthenticationManager traverse the collection can find our custom SmsAuthenticationProvider
When SmsAuthenticationProvider authentication, you need to call UserDetailsService according to cell number of storing user information (loadUserByUsername), So we also need a custom SmsUserDetailsService
Let’s implement it one by one (actually, COPY the code of the component corresponding to the login process of the user name and password to change it).
SmsAuthenticationToken
package top.zhenganwen.security.core.verifycode.sms;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/ * * *@author zhenganwen
* @date 2019/8/30
* @desc SmsAuthenticationToken
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
// Before authentication, the mobile phone number entered by the user is saved. After authentication, the user details stored in the backend are saved
private final Object principal;
// ~ Constructors
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
/** * Call this method before authentication to encapsulate request parameters into an unauthenticated token => authRequest **@paramPhoneNumber mobile phoneNumber */
public SmsAuthenticationToken(Object phoneNumber) {
super(null);
this.principal = phoneNumber;
setAuthenticated(false);
}
SuccessToken => successToken **@paramPrincipal User details *@paramAuthorities */
public SmsAuthenticationToken(Object principal, Object credentials, Collection
authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
// User name Password The login credential is the password, but the verification code does not transmit the password
@Override
public Object getCredentials(a) {
return null;
}
@Override
public Object getPrincipal(a) {
return this.principal; }}Copy the code
SmsAuthenticationFilter
package top.zhenganwen.security.core.verifycode.sms;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/ * * *@author zhenganwen
* @date 2019/8/30
* @desc SmsAuthenticationFilter
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
public static final String SPRING_SECURITY_FORM_PHONE_NUMBER_KEY = "phoneNumber";
private String phoneNumberParameter = SPRING_SECURITY_FORM_PHONE_NUMBER_KEY;
private boolean postOnly = true;
// ~ Constructors
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
public SmsAuthenticationFilter(a) {
super(new AntPathRequestMatcher("/auth/sms"."POST"));
}
// ~ Methods
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
@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 phoneNumber = obtainPhoneNumber(request);
if (phoneNumber == null) {
phoneNumber = "";
}
phoneNumber = phoneNumber.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phoneNumber);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* Enables subclasses to override the composition of the phoneNumber, such as by
* including additional values and a separator.
*
* @param request so that request attributes can be retrieved
*
* @return the phoneNumber that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
protected String obtainPhoneNumber(HttpServletRequest request) {
return request.getParameter(phoneNumberParameter);
}
/**
* Sets the parameter name which will be used to obtain the phoneNumber from the login
* request.
*
* @param phoneNumberParameter the parameter name. Defaults to "phoneNumber".
*/
public void setPhoneNumberParameter(String phoneNumberParameter) {
Assert.hasText(phoneNumberParameter, "phoneNumber parameter must not be empty or null");
this.phoneNumberParameter = phoneNumberParameter;
}
/** * Defines whether only HTTP POST requests will be allowed by this filter. If set to * true, and an authentication request is received which is not a POST request, an * exception will be raised immediately and authentication will not be attempted. The * unsuccessfulAuthentication() method will be called as if handling a failed * authentication. * * Defaults to true but may be overridden by subclasses. */
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getPhoneNumberParameter(a) {
returnphoneNumberParameter; }}Copy the code
SmsAuthenticationProvider
package top.zhenganwen.security.core.verifycode.sms;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/ * * *@author zhenganwen
* @date 2019/8/30
* @desc SmsAuthenticationProvider
*/
public class SmsAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public SmsAuthenticationProvider(a) {}/** * This method is called by AuthenticationManager to verify authentication and returns an authenticated {@link Authentication}
* @param authentication
* @return* /
@Override
public Authentication authenticate(Authentication authentication){
// User name password Login mode Check whether the password imported from the front end is consistent with that stored in the back end
// However, if the verification of the SMS verification code is stored here, it cannot be reused. For example, users may need to send the SMS verification code to access the "My Wallet" service after logging in
// Therefore, the verification logic of the SMS verification code is extracted separately into a filter (reserved for later implementation), which directly returns a successful authentication
if (authentication instanceof SmsAuthenticationToken == false) {
throw new IllegalArgumentException("Only SmsAuthenticationToken authentication is supported");
}
SmsAuthenticationToken authRequest = (SmsAuthenticationToken) authentication;
UserDetails userDetails = getUserDetailsService().loadUserByUsername((String) authentication.getPrincipal());
SmsAuthenticationToken successfulAuthentication = new SmsAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
return successfulAuthentication;
}
/** * The authenticate method calls this method when traversing all AuthenticationProviders to determine whether the current AuthenticationProvider is true Validation for a specific Authentication * * override this method to support validation for {@linkSmsAuthenticationToken} authentication verification *@paramToken types supported by Clazz *@return* /
@Override
public boolean supports(Class
clazz) {
// If the class passed is SmsAuthenticationToken or a subclass of it
return SmsAuthenticationToken.class.isAssignableFrom(clazz);
}
public UserDetailsService getUserDetailsService(a) {
return userDetailsService;
}
/** * Provides dynamic injection of UserDetailsService *@return* /
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService; }}Copy the code
SmsDetailsService
package top.zhenganwen.security.core.verifycode.sms;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import java.util.Objects;
/ * * *@author zhenganwen
* @date 2019/8/30
* @desc SmsUserDetailsService
*/
@Service
public class SmsUserDetailsService implements UserDetailsService {
/** * Query the user based on the login name, which is the mobile phone number **@param phoneNumber
* @return
* @throws PhoneNumberNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String phoneNumber) throws PhoneNumberNotFoundException {
// DAO should actually be called to query the user based on the phone number
if (Objects.equals(phoneNumber, "12345678912") = =false) {
/ / not to
throw new PhoneNumberNotFoundException();
}
The / / check
// Use the implementation of UserDetails provided by Security to simulate the detected User. In your project, you can implement the UserDetails interface using the User entity class, which returns the detected User entity object directly
return new User("anwen"."123456", AuthorityUtils.createAuthorityList("admin"."super_admin")); }}Copy the code
Note that when this class is added, there are two UserDetails in the container. The @autowire UserDetails should be replaced with @Autowire customDetailsService, otherwise an error will be reported
SmsLoginConfig
We have implemented the components for each step, and now we need to write a configuration class to string them together and tell Security that these custom components exist. Since SMS login can be used on both PC and mobile terminals, it is defined in security-core
package top.zhenganwen.security.core.verifycode.sms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/ * * *@author zhenganwen
* @date 2019/8/30
* @desc SmsSecurityConfig
*/
@Component
public class SmsLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
@Autowired
AuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
AuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
UserDetailsService smsUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
// The authentication filter will ask AuthenticationManager to authenticate authRequest, so we need to inject AuthenticatonManager into it, but the instance is managed by Security and we need to get it via getSharedObject
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// Authentication succeeds/fails with the same handler as before
smsAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
smsAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
/ / will be injected into SmsAuthenticationProvider SmsUserDetailsService
smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService);
/ / will SmsAuthenticationProvider added to the Security management of AuthenticationProvider collection
http.authenticationProvider(smsAuthenticationProvider)
/ / note to add to UsernamePasswordAuthenticationFilter, custom authentication filter should be added to the later, the custom authentication code of the filter should be added to it before.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
test
Login to /login. HTML and click send to view the SMS verification code output by the console. Then login to /login. HTML.
However, the user name and password login failed! The Bad Credentials are incorrect, so I debug the breakpoint at the password verification site:
DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource ! =null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if(! passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials")); }}Copy the code
PasswordEncoder is PlaintextPasswordEncoder instead of BCryptPasswordEncoder. Why?
We need to go back to the source to see when the passwordEncoder was assigned, Alt + F7 in this file to see when the setPasswordEncoder(Object passwordEncoder) method of that class was called, It’s going to be initialized to PlaintextPasswordEncoder in the constructor; But that’s not what we want. We want to see why the BCryptPasswordEncoder that was injected before adding the SMS verification code login function works. Ctrl + Alt + F7 searches the entire project and library for setPasswordEncoder(Object passwordEncoder) calls and finds the following clues:
InitializeUserDetailsManagerConfigurer
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
if (auth.isConfigured()) {
return;
}
UserDetailsService userDetailsService = getBeanOrNull(
UserDetailsService.class);
if (userDetailsService == null) {
return;
}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if(passwordEncoder ! =null) {
provider.setPasswordEncoder(passwordEncoder);
}
auth.authenticationProvider(provider);
}
/ * * *@return* /
private <T> T getBeanOrNull(Class<T> type) {
String[] userDetailsBeanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
.getBeanNamesForType(type);
if(userDetailsBeanNames.length ! =1) {
return null;
}
return InitializeUserDetailsBeanManagerConfigurer.this.context
.getBean(userDetailsBeanNames[0], type);
}
Copy the code
Originally, in and tried to find if we inject other PasswordEncoder instance DaoAuthenticationProvider injection before we configure BCryptPasswordEncoder, will from the container get populated UserDetails instance, If there are no instances in the container or the number of instances is greater than 1, then it returns.
Turns out, when we implemented SMS verification code login, The @Component of the SmsUserDetailsService annotation results in the existence of two UserDetailsService instances in the container, SmsUserDetailsService and the previous customUserDetailsService. So the above code code are not executed after 12, that is to say we have no injection CustomUserDetailsService and BCryptPasswordEncoder to DaoAuthenticationProvider.
As for why, before verifying the password, DaoAuthenticationProvider of enclosing getUserDetailsService (.) loadUserByUsername (username) can still call CustomUserDetailsService Cu is and why Rather than SmsUserDetialsService stomUserDetailsService injected by DaoAuthenticationProvider, remains to be analyzed
Now that the problem has been identified (there are two UserDetailsService instances in the container), the simple solution is to remove the @Component of SmsUserDetailsService and simply create a new one when configuring the SMS login tandem Component
//@Component
public class SmsUserDetailsService implements UserDetailsService {
Copy the code
@Component
public class SmsLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
@Autowired
AuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
AuthenticationFailureHandler customAuthenticationFailureHandler;
// @Autowired
// SmsUserDetailsService smsUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
smsAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
// Do it yourself
SmsUserDetailsService smsUserDetailsService = newSmsUserDetailsService(); smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService); http.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
Retest both login methods, both can pass!
SMS verification code filter
As mentioned in the previous section, for reuse, we should put the verification logic of the SMS verification code into a separate filter. Here we can refer to the graphic verification code filter written before, and copy it for modification
package top.zhenganwen.security.core.verifycode.filter;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.exception.VerifyCodeException;
import top.zhenganwen.security.core.verifycode.po.VerifyCode;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import static top.zhenganwen.security.core.verifycode.constont.VerifyCodeConstant.SMS_CODE_SESSION_KEY;
/ * * *@author zhenganwen
* @date 2019/8/24
* @desc VerifyCodeAuthenticationFilter
*/
@Component
public class SmsCodeAuthenticationFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler customAuthenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
private Set<String> uriPatternSet = new HashSet<>();
// uri matching tool class, help us to do similar /user/1 to /user/* matching
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet(a) throws ServletException {
super.afterPropertiesSet();
String uriPatterns = securityProperties.getCode().getSms().getUriPatterns();
if (StringUtils.isNotBlank(uriPatterns)) {
String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(uriPatterns, ",");
uriPatternSet.addAll(Arrays.asList(strings));
}
uriPatternSet.add("/auth/sms");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,
IOException {
for (String uriPattern : uriPatternSet) {
// If there is a match, the verification code needs to be intercepted
if (antPathMatcher.match(uriPattern, request.getRequestURI())) {
try {
this.validateVerifyCode(new ServletWebRequest(request));
} catch (VerifyCodeException e) {
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
break;
}
}
filterChain.doFilter(request, response);
}
// Intercepts the user login request and reads the saved SMS verification code from the Session for comparison with the verification code submitted by the user
private void validateVerifyCode(ServletWebRequest request){
String smsCode = (String) request.getParameter("smsCode");
if (StringUtils.isBlank(smsCode)) {
throw new VerifyCodeException("Verification code cannot be empty.");
}
VerifyCode verifyCode = (VerifyCode) sessionStrategy.getAttribute(request, SMS_CODE_SESSION_KEY);
if (verifyCode == null) {
throw new VerifyCodeException("Captcha does not exist");
}
if (verifyCode.isExpired()) {
throw new VerifyCodeException("Verification code has expired. Please refresh the page.");
}
if (StringUtils.equals(smsCode,verifyCode.getCode()) == false) {
throw new VerifyCodeException("Verification code error"); } sessionStrategy.removeAttribute(request, SMS_CODE_SESSION_KEY); }}Copy the code
Then remember to add it to the security filter chain, and only before all authentication filters:
SecurityBrowserConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/auth/require")
.loginProcessingUrl("/auth/login")
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600)
.userDetailsService(customUserDetailsService)
// You can configure the name attribute of the page selection box
// .rememberMeParameter()
.and()
.authorizeRequests()
.antMatchers(
"/auth/require",
securityProperties.getBrowser().getLoginPage(),
"/verifyCode/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.apply(smsLoginConfig);
}
Copy the code
Finally, modify login URL/ Auth/SMS and smsCode in login. HTML:
<form action="/auth/login" method="post">User name:<input type="text" name="username" value="admin">Password:<input type="password" name="password" value="123">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=600" alt="">
<input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
<hr/>
<form action="/auth/sms" method="post">Mobile phone no. :<input type="text" name="phoneNumber" value="12345678912">Verification code:<input type="text" name="smsCode"><a href="/verifyCode/sms? phoneNumber=12345678912">Click on send</a>
<input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
Copy the code
Refactoring – Eliminates duplicate code
Before, we copied the code of graphic verification code filter and changed it into SMS verification code filter. The main process of these two classes is the same, but the specific implementation is slightly different (reading and writing the verification code object corresponding to different keys from the Session), which can be extracted by using template method
There are many literal mana values in our code, and we should eliminate them as much as possible, extract them as constants or configuration properties, and reference them where they are needed, so that we don’t have to forget the mana value and cause exceptions when we need to change it later. For example, if you simply change.loginPage(“/auth/require”) to.loginPage(“/authentication/require”), Rather than by changing the BrowserSecurityController @ RequestMapping (“/auth/require “), will cause the program function problems
We can package the codes related to system configuration into modules and put them into the corresponding configuration classes in security-core. Security-browser and security-app only leave their own specific configuration (e.g., remember-me mode of writing token to cookie should be put in security-Browser, Security-app corresponds to the configuration mode of mobile terminal remember-me). Finally, security-browser and security-app can reference the general configuration of security-core through HTTP. Apply to realize code reuse
As soon as there are two or more identical pieces of code in your project, your nose should be sharp enough to spot the least noticeable and most noticeable bad code smells, and you should be looking for ways to refactor them in a timely manner, rather than waiting for the system to become too big to change
Mana reconstruction
package top.zhenganwen.security.core.verifycode.filter;
public enum VerifyCodeType {
SMS{
@Override
public String getVerifyCodeParameterName(a) {
return SecurityConstants.DEFAULT_SMS_CODE_PARAMETER_NAME;
}
},
IMAGE{
@Override
public String getVerifyCodeParameterName(a) {
returnSecurityConstants.DEFAULT_IMAGE_CODE_PARAMETER_NAME; }};public abstract String getVerifyCodeParameterName(a);
}
Copy the code
package top.zhenganwen.security.core;
public interface SecurityConstants {
/** * Form password login URL */
String DEFAULT_FORM_LOGIN_URL = "/auth/login";
/** * SMS login URL */
String DEFAULT_SMS_LOGIN_URL = "/auth/sms";
/** * Front-end graphics verification code parameter name */
String DEFAULT_IMAGE_CODE_PARAMETER_NAME = "imageCode";
/** * Front-end SMS verification code parameter name */
String DEFAULT_SMS_CODE_PARAMETER_NAME = "smsCode";
/** * Graphic captcha cached in Session key */
String IMAGE_CODE_SESSION_KEY = "IMAGE_CODE_SESSION_KEY";
/** * THE SMS verification code is cached in key */ of the Session
String SMS_CODE_SESSION_KEY = "SMS_CODE_SESSION_KEY";
/** * Validator bean name suffix */
String VERIFY_CODE_VALIDATOR_NAME_SUFFIX = "CodeValidator";
/** * If you do not log in to the protected URL, jump to this */
String FORWARD_TO_LOGIN_PAGE_URL = "/auth/require";
/** * The user clicks to send the verification code to invoke the service */
String VERIFY_CODE_SEND_URL = "/verifyCode/**";
}
Copy the code
The verification code filter was reconstructed. Procedure
VerifyCodeValidatorFilter
Is responsible for intercepting requests that require captcha verificationVerifyCodeValidator
, using template method, abstract verification logic of the verification codeVerifyCodeValidatorHolder
Using Spring’s dependency lookup, aggregate all of theVerifyCodeValidator
Implementation class (specific verification logic of various verification codes) to provide external verification based on the verification code typebean
The method of
Login. HTML, where the graphic captcha parameter is changed to imageCode
<form action="/auth/login" method="post">User name:<input type="text" name="username" value="admin">Password:<input type="password" name="password" value="123">Verification code:<input type="text" name="imageCode"><img src="/verifyCode/image? width=600" alt="">
<input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
<hr/>
<form action="/auth/sms" method="post">Mobile phone no. :<input type="text" name="phoneNumber" value="12345678912">Verification code:<input type="text" name="smsCode"><a href="/verifyCode/sms? phoneNumber=12345678912">Click on send</a>
<input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
Copy the code
VerifyCodeValidateFilter:
package top.zhenganwen.security.core.verifycode.filter;
import static top.zhenganwen.security.core.SecurityConstants.DEFAULT_SMS_LOGIN_URL;
@Component
public class VerifyCodeValidateFilter extends OncePerRequestFilter implements InitializingBean {
// Failed to authenticate the processor
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
// session reads and writes tools
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
// Map the URI and type of the verification code, for example, /auth/login -> Graphic verification code /auth/ SMS -> SMS verification code
private Map<String, VerifyCodeType> uriMap = new HashMap<>();
@Autowired
private SecurityProperties securityProperties;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private VerifyCodeValidatorHolder verifyCodeValidatorHolder;
@Override
public void afterPropertiesSet(a) throws ServletException {
super.afterPropertiesSet();
uriMap.put(SecurityConstants.DEFAULT_FORM_LOGIN_URL, VerifyCodeType.IMAGE);
putUriPatterns(uriMap, securityProperties.getCode().getImage().getUriPatterns(), VerifyCodeType.IMAGE);
uriMap.put(SecurityConstants.DEFAULT_SMS_LOGIN_URL, VerifyCodeType.SMS);
putUriPatterns(uriMap, securityProperties.getCode().getSms().getUriPatterns(), VerifyCodeType.SMS);
}
private void putUriPatterns(Map<String, VerifyCodeType> urlMap, String uriPatterns, VerifyCodeType verifyCodeType) {
if (StringUtils.isNotBlank(uriPatterns)) {
String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(uriPatterns, ",");
for(String string : strings) { urlMap.put(string, verifyCodeType); }}}@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException
, IOException {
try {
checkVerifyCodeIfNeed(request, uriMap);
} catch (VerifyCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
filterChain.doFilter(request, response);
}
private void checkVerifyCodeIfNeed(HttpServletRequest request, Map<String, VerifyCodeType> uriMap) {
String requestUri = request.getRequestURI();
Set<String> uriPatterns = uriMap.keySet();
for (String uriPattern : uriPatterns) {
if (antPathMatcher.match(uriPattern, requestUri)) {
VerifyCodeType verifyCodeType = uriMap.get(uriPattern);
verifyCodeValidatorHolder.getVerifyCodeValidator(verifyCodeType).validateVerifyCode(new ServletWebRequest(request), verifyCodeType);
break; }}}}Copy the code
VerifyCodeValidator
package top.zhenganwen.security.core.verifycode.filter;
import java.util.Objects;
public abstract class VerifyCodeValidator {
protected SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private VerifyCodeValidatorHolder verifyCodeValidatorHolder;
/** * Verification code * 1. Obtain the verification code * 2 from the request. Obtain the verification code from the server * 3. Verify the verification code * 4. Remove the verification code from the server successfully@param request
* @param verifyCodeType
* @throws VerifyCodeException
*/
public void validateVerifyCode(ServletWebRequest request, VerifyCodeType verifyCodeType) throws VerifyCodeException {
String requestCode = getVerifyCodeFromRequest(request, verifyCodeType);
VerifyCodeValidator codeValidator = verifyCodeValidatorHolder.getVerifyCodeValidator(verifyCodeType);
if (Objects.isNull(codeValidator)) {
throw new VerifyCodeException("Unsupported verification type:" + verifyCodeType);
}
VerifyCode storedVerifyCode = codeValidator.getStoredVerifyCode(request);
codeValidator.validate(requestCode, storedVerifyCode);
codeValidator.removeStoredVerifyCode(request);
}
/** * To verify whether the verification code is expired, simple text comparison is performed by default. Subclasses can be overridden to verify the incoming plaintext verification code and the stored ciphertext verification code **@param requestCode
* @param storedVerifyCode
*/
private void validate(String requestCode, VerifyCode storedVerifyCode) {
if (Objects.isNull(storedVerifyCode) || storedVerifyCode.isExpired()) {
throw new VerifyCodeException("Verification code is invalid. Please regenerate it.");
}
if (StringUtils.isBlank(requestCode)) {
throw new VerifyCodeException("Verification code cannot be empty.");
}
if (StringUtils.equalsIgnoreCase(requestCode, storedVerifyCode.getCode()) == false) {
throw new VerifyCodeException("Verification code error"); }}/** * It is up to subclasses to remove captcha from Session or from other caching methods **@param request
*/
protected abstract void removeStoredVerifyCode(ServletWebRequest request);
It is up to subclasses to read captcha from Session or from other caches **@param request
* @return* /
protected abstract VerifyCode getStoredVerifyCode(ServletWebRequest request);
/** * Takes the captcha argument from the request by default and can be overridden by subclasses **@param request
* @param verifyCodeType
* @return* /
private String getVerifyCodeFromRequest(ServletWebRequest request, VerifyCodeType verifyCodeType) {
try {
return ServletRequestUtils.getStringParameter(request.getRequest(), verifyCodeType.getVerifyCodeParameterName());
} catch (ServletRequestBindingException e) {
throw new VerifyCodeException("Illegal request, please attach captcha parameter"); }}}Copy the code
ImageCodeValidator
package top.zhenganwen.security.core.verifycode.filter;
@Component
public class ImageCodeValidator extends VerifyCodeValidator {
@Override
protected void removeStoredVerifyCode(ServletWebRequest request) {
sessionStrategy.removeAttribute(request, SecurityConstants.IMAGE_CODE_SESSION_KEY);
}
@Override
protected VerifyCode getStoredVerifyCode(ServletWebRequest request) {
return(VerifyCode) sessionStrategy.getAttribute(request, SecurityConstants.IMAGE_CODE_SESSION_KEY); }}Copy the code
SmsCodeValidator
package top.zhenganwen.security.core.verifycode.filter;
@Component
public class SmsCodeValidator extends VerifyCodeValidator {
@Override
protected void removeStoredVerifyCode(ServletWebRequest request) {
sessionStrategy.removeAttribute(request, SecurityConstants.SMS_CODE_SESSION_KEY);
}
@Override
protected VerifyCode getStoredVerifyCode(ServletWebRequest request) {
return(VerifyCode) sessionStrategy.getAttribute(request,SecurityConstants.SMS_CODE_SESSION_KEY); }}Copy the code
VerifyCodeValidatorHolder
package top.zhenganwen.security.core.verifycode.filter;
@Component
public class VerifyCodeValidatorHolder {
@Autowired
private Map<String, VerifyCodeValidator> verifyCodeValidatorMap = new HashMap<>();
public VerifyCodeValidator getVerifyCodeValidator(VerifyCodeType verifyCodeType) {
VerifyCodeValidator verifyCodeValidator =
verifyCodeValidatorMap.get(verifyCodeType.toString().toLowerCase() + SecurityConstants.VERIFY_CODE_VALIDATOR_NAME_SUFFIX);
if (Objects.isNull(verifyCodeType)) {
throw new VerifyCodeException("Unsupported captchas :" + verifyCodeType);
}
returnverifyCodeValidator; }}Copy the code
SecurityBrowserConfig
@Autowire
VerifyCodeValidatorFilter verifyCodeValidatorFilter;
http
// .addFilterBefore(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(verifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600)
.userDetailsService(customUserDetailsService)
.and()
.authorizeRequests()
.antMatchers(
SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
securityProperties.getBrowser().getLoginPage(),
SecurityConstants.VERIFY_CODE_SEND_URL).permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.apply(smsLoginConfig);
Copy the code
System Configuration Reconstruction
security-core
package top.zhenganwen.security.core.config;
@Component
public class SmsLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
@Autowired
AuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
AuthenticationFailureHandler customAuthenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
smsAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
SmsUserDetailsService smsUserDetailsService = newSmsUserDetailsService(); smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService); http.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
package top.zhenganwen.security.core.config;
@Component
public class VerifyCodeValidatorConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
@Autowired
private VerifyCodeValidateFilter verifyCodeValidateFilter;
@Override
public void configure(HttpSecurity builder) throws Exception { builder.addFilterBefore(verifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
security-browser
package top.zhenganwen.securitydemo.browser;
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(a) {
return new BCryptPasswordEncoder();
}
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService customUserDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository(a) {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Autowired
SmsLoginConfig smsLoginConfig;
@Autowired
private VerifyCodeValidatorConfig verifyCodeValidatorConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
// Enable the verification filter
http.apply(verifyCodeValidatorConfig);
// Enable the SMS login filter
http.apply(smsLoginConfig);
http
// Enable the form password login filter
.formLogin()
.loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
// The browser application-specific configuration saves the token generated after login in a cookie
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600)
.userDetailsService(customUserDetailsService)
.and()
// Browser application-specific configuration.authorizeRequests() .antMatchers( SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL, securityProperties.getBrowser().getLoginPage(), SecurityConstants.VERIFY_CODE_SEND_URL).permitAll() .anyRequest().authenticated().and() .csrf().disable(); }}Copy the code
Use Spring Social to develop third-party logins
Introduction to the OAuth protocol
background
Sometimes applications cooperate with each other to achieve win-win results. For example, the popular micro channel public number, micro channel small program. On the one hand, developers of public accounts and small programs can attract wechat users with rich content to improve the user retention rate of wechat. On the other hand, public accounts and small programs can use wechat’s strong user base for their own services
When problems come, if you use the traditional way, the small program to achieve the user information to the user application to claim your account password (such as skin care small programs need to read the user’s WeChat photo albums to beautify), not to mention the user give not to, even if the user to, so will still exist the following questions (facial small programs, for example)
-
Access permissions
It is impossible to control the access of the small program, saying that it only reads wechat photo albums. Who knows if he will check his wechat friends and use wechat wallet after taking the account password
-
Authorized limitation
Once the mini program obtains the user’s account password, the user cannot control the authorization. The mini program will not use the account password to log in illegally in the future. The user can only change the password after each authorization
-
reliability
If a user authorizes multiple applets in this way, once the applets disclose the user password, the user faces the risk of number theft
OAuth solution
When a user agrees to authorize a third party application (such as wechat applet relative to wechat user), the third party application will only be given a token (through which the third party application can access the user’s specific data resources). This token is created to solve the above problem:
- The token is time-limited and only valid for a specified period of time, which solves the problem of authorization limitation
- Tokens can only access specific resources granted by the user, which solves the problem of access rights
- A token is a random string that is valid for a short time and has no meaning when expired, which solves the reliability problem
OAuth protocol running process
Let’s start with a few of the roles and responsibilities involved:
Provider
Service providers such as wechat and QQ have huge amounts of user dataAuthorization Server
, the authentication server is generated by the authentication server after the user agrees to authorizetoken
To a third party applicationResource Server
To store resources required by third-party applicationstoken
If correct, the corresponding resources are open to third-party applications
Resource Owner
, the resource owner. For example, the wechat user is the resource owner of the wechat album. The photos are taken by the wechat user but stored on the wechat serverClient
, third-party applications that rely on service providers with a strong user base for traffic diversion
The second step above also involves several authorization modes:
- Authorization Code Mode (
authorization code
) - Password mode (
resource owner password credentials
) - Customer Card Mode (
client credentials
) - Simplified mode (
implicit
)
This chapter and the next chapter (APP) will respectively introduce the first two modes in detail. At present, almost most social platforms on the Internet, such as QQ, Weibo, Taobao and other service providers, adopt the authorization code mode
Authorization code mode Authorization process
For example, when we visit a social networking site, we do not want to register the users of the site but directly use QQ to log in. The figure is the approximate sequence diagram of QQ joint login developed by the social networking site as a third-party application using OAuth protocol
The authorization code pattern is widely used for the following two reasons:
- The behavior of user consent authorization is confirmed on the authentication server, which is more transparent than the confirmation on the third-party application client in the other three modes (the client can forge user consent authorization)
- The authentication server does not return the token directly, but the authorization code first. Static sites like some may be used
implicit
The pattern lets the authentication server return the token directly and then make an AJAX call to the resource server interface on the page. The authentication server connects to a third-party application servertoken
It is used to call back the third-party application interface that has been agreed with the third-party applicationtoken
Therefore, alltoken
Are stored on the server); The latter is the client that the authentication server connects to third-party applications such as browsers,token
Security risks exist if the client is directly sent to the client
This is why the mainstream service providers are adopting the authorization code model, because the authorization process is more complete and secure.
The fundamentals of Spring Social
Spring Social encapsulates the authorization process described in the sequence diagram into specific classes and interfaces. OAuth protocol has two versions, foreign very early use so popular OAuth1, and domestic use is relatively late so basically OAuth2, this chapter is also based on OAuth2 to integrate QQ, wechat login function.
The main components of Spring Social are shown below:
OAuth2Operations
The encapsulation goes back to us from requesting user authorization to the authentication servicetoken
The whole process.OAuth2Template
Is the default implementation provided for us, and the process is basically fixed without our involvement- Api, package get
token
Then we call the resource server interface to get user information, which needs to be defined by ourselves, after all, the framework does not know which open platform we want to access, but it also provides us with an abstractionAbstractOAuth2ApiBinding
AbstractOAuth2ServiceProvider
The integrationOAuth2Operation
andApi
, string fetchtoken
And taketoken
Two processes for accessing user resourcesConnection
Since the data structure of user information returned by different service providers is inconsistent, we need to use the adapterApiAdapter
To unify itConnection
This data structure can be viewed as the user’s entity in the service providerOAuth2ConnectionFactory
The integrationAbstractOAuth2ServiceProvider
andApiAdapter
To complete the process of user authorization and obtaining user information entitiesUsersConnectionRepository
, our system generally has its own user table, how to access the system of user entitiesConnection
And our own user entitiesUser
That’s how it works, how it works for ususerId
toConnection
The mapping of
Develop QQ login function
To be continued…
The resources
Video tutorial links: pan.baidu.com/s/1wQWD4wE0… Extraction code: Z6ZI