Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
Hello ya, I am ning Zaichun, blogger, come on!!
I wonder if you have ever thought of using Spring Security to implement various login methods. This time, my friend mentioned some login requirements to me. Besides the original account and password login, I need to implement phone verification code and email verification code login. Let me achieve third-party login, such as Gitee, Github, etc.
This paper mainly explains how Security can realize email and phone verification code login on the basis of account password and without changing the original service.
Cover: clouds seen from the playground in the evening
π± π» preface:
Last article I wrote the Security login detailed process, source code, analysis. By mastering this login process, we can better customize Security operations.
I wrote this article before, also read a lot of blogger’s article, write very good, have to the source code aspect analysis, also have to some related design concept understanding article.
This is suitable for those who have learned Security for some time and have a good understanding of it. However, it is not so friendly to me and other young people who are eager to solve the current problem. π
I. π€ΈβοΈ Theoretical knowledge
So let’s think about what the process looks like, right?
- Enter the email number to obtain the verification code
- Enter the obtained verification code to log in (Login interface:
/email/login
The default cannot be used here/login
Because we are extension) - In custom filters
EmailCodeAuthenticationFilter
To check whether the verification code is correct and whether the email account is empty - Encapsulate it into one that requires authentication
Authentication
, here we customize the implementation asEmailCodeAuthenticationToken
. - will
Authentiction
To pass toAuthenticationManager
In the interfaceauthenticate
Method for authentication processing AuthenticationManager
The default implementation class isProviderManager
οΌProviderManager
And entrusted toAuthenticationProvider
For processing- Let’s make a custom one
EmailCodeAuthenticationProvider
implementationAuthenticationProvider
To implement authentication. - The custom of
EmailCodeAuthenticationFilter
inheritedAbstractAuthenticationProcessingFilter
An abstract class,AbstractAuthenticationProcessingFilter
insuccessfulAuthentication
Method is used to process the login successSecurityContextHolder.getContext().setAuthentication()
Method will beAuthentication
Authentication information object bound toSecurityContext
The security context. - In fact, there are two ways to deal with the authentication after passing, one is to directly rewrite the filter
successfulAuthentication
The other is implementationAuthenticationSuccessHandler
To process the authentication pass. - Authentication failures are the same and can be overridden
unsuccessfulAuthentication
Method can also be implementedAuthenticationFailureHandler
To handle authentication failures.
That’s the general process. From this process, we can see that the following components need to be rewritten:
EmailCodeAuthenticationFilter
: mail authentication login filterEmailCodeAuthenticationToken
: Authentication tokenEmailCodeAuthenticationProvider
: Email identity authentication processingAuthenticationSuccessHandler
: Processes the successful login operationAuthenticationFailureHandler
: Processes login failures
Next, I am imitating the source code to write my code, I suggest you can use when, to see more, I am here to remove some of the code is not related to this.
Come on!!!!!
Second, the π± π EmailCodeAuthenticationFilter
We need to rewrite EmailCodeAuthenticationFilter, actual inherited AbstractAuthenticationProcessingFilter abstract class, we can’t write, Can first take a look at its default implementation UsernamePasswordAuthenticationFilter is how are you, copy homework, it is everybody’s strengths.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login"."POST");
// The parameters passed from the foreground
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// Initialize a user password authentication filter. The default login URI is /login. The request mode is POST
public UsernamePasswordAuthenticationFilter(a) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
/** Perform the actual authentication. The implementation should do one of the following: 1. Return the populated authentication token for the authenticated user, indicating that the authentication was successful; 2. Before returning, the implementation should perform any additional work needed to complete the process. 3. If the authentication process fails, throw AuthenticationException */
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && ! request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: "+ request.getMethod()); } String username = obtainUsername(request); username = (username ! =null)? username :""; username = username.trim(); String password = obtainPassword(request); password = (password ! =null)? password :"";
/ / generated UsernamePasswordAuthenticationToken later to authenticate in the AuthenticationManager for certification
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// You can put some other information in
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//set and get methods
}
Copy the code
Let’s copy the homework:
package com.crush.security.auth.email_code;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
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.security.web.util.matcher.RequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
/ * * *@Author: crush
* @Date: 2021-09-08 21:13
* version 1.0
*/
public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/** * The name of the argument passed from the front - used in request.getParameter to get */
private final String DEFAULT_EMAIL_NAME="email";
private final String DEFAULT_EMAIL_CODE="e_code";
@Autowired
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
/** * Only post */
private boolean postOnly = true;
/** * Creates the Filter * that is, the url filtered by Filter */
public EmailCodeAuthenticationFilter(a) {
super(new AntPathRequestMatcher("/email/login"."POST"));
}
/** * filter obtains the user name (email) and password (verification code) to attach to the token, and then gives the token to the provider for authorization */
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if(postOnly && ! request.getMethod().equals("POST") ){
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}else{
String email = getEmail(request);
if(email == null){
email = "";
}
email = email.trim();
// If the captcha is not equal, deliberately error the token and go through the springSecurity error process
boolean flag = checkCode(request);
/ / encapsulation token
EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());
this.setDetails(request,token);
// Submit the certificate to manager
return this.getAuthenticationManager().authenticate(token); }}/** * get the header information and let the appropriate provider validate it */
public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
/** * get the incoming Email message */
public String getEmail(HttpServletRequest request ){
String result= request.getParameter(DEFAULT_EMAIL_NAME);
return result;
}
/** * Determine the incoming verification code and the verification code in the session */
public boolean checkCode(HttpServletRequest request ){
String code1 = request.getParameter(DEFAULT_EMAIL_CODE);
System.out.println("code1**********"+code1);
// TODO writes another link to generate a captcha that is stored in Redis when it is generated
// The verification code in TODO is written in Redis
if(code1.equals("123456")) {return true;
}
return false;
}
// set, get...
}
Copy the code
Third, π€ EmailCodeAuthenticationToken
We are inherited AbstractAuthenticationToken EmailCodeAuthenticationToken, in the same way, we then look at the AbstractAuthenticationToken default implementation is what kind of line.
/ * * * /
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// This is the password of the account
private final Object principal;
private Object credentials;
SetAuthenticated (false) = unsignalable token */
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
SetAuthenticated (true) set to trusted token */
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection
authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials(a) {
return this.credentials;
}
@Override
public Object getPrincipal(a) {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(! isAuthenticated,"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials(a) {
super.eraseCredentials();
this.credentials = null; }}Copy the code
Daily copy operation ha:
/ * * *@Author: crush
* @Date: 2021-09-08 21:13
* version 1.0
*/
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {
/** * principal refers to the email address (when not authenticated) */
private final Object principal;
public EmailCodeAuthenticationToken(Object principal) {
super((Collection) null);
this.principal = principal;
setAuthenticated(false);
}
public EmailCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials(a) {
return null;
}
@Override
public Object getPrincipal(a) {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false); }}}Copy the code
That’s pretty easy. π¨ π»
Four, π± π EmailCodeAuthenticationProvider
Custom EmailCodeAuthenticationProvider is to implement AuthenticationProvider interface, chaozuoye have to learn to look at the source code. Let’s keep going.
4.1, see AbstractUserDetailsAuthenticationProvider first, let’s imitate
The AuthenticationProvider interface has many implementation classes, so I won’t go into detail. We need to look at directly AbstractUserDetailsAuthenticationProvider, this class is designed to respond to UsernamePasswordAuthenticationToken authentication request. But it’s an abstract class, but there’s really only one step that’s implemented in its implementation class, very simple, and I’ll talk about that later.
In this source code I put and check related to some operations to delete, leaving only a few key points, let’s take a look.
/ / this class aims to response UsernamePasswordAuthenticationToken authentication request.
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider.InitializingBean.MessageSourceAware {
protected final Log logger = LogFactory.getLog(getClass());
private UserCache userCache = new NullUserCache();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports"."Only UsernamePasswordAuthenticationToken is supported"));
// Get the user name
String username = determineUsername(authentication);
// Check whether the cache exists
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// There is no retrieveUser implemented through the word class in the cache to retrieve from the database and return a UserDetails object
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// Perform relevant checks because it may be pulled from the cache and not be up to date
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if(! cacheWasUsed) {throw ex;
}
// Failed to pass the check and retrieve the latest data
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// Check again
this.postAuthenticationChecks.check(user);
// Store it in the cache
if(! cacheWasUsed) {this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// Create a trusted identity token to return
return createSuccessAuthentication(principalToReturn, authentication, user);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null)?"NONE_PROVIDED" : authentication.getName();
}
In short / * * is created an authenticated UsernamePasswordAuthenticationToken * /
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
/** allows subclasses to actually retrieve UserDetails from an implementation-specific location, with the option to throw an AuthenticationException immediately if the credentials provided are incorrect (if you need to bind to a resource as a user to get or generate a UserDetails) */
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
/ /...
// In a nutshell: Of course, sometimes we have multiple different 'authenticationProviders' that support different' Authentication 'objects, So when a concrete 'AuthenticationProvier' is passed inside the 'ProviderManager', It selects the supported provider from the AuthenticationProvider list to authenticate the corresponding Authentication object
@Override
public boolean supports(Class
authentication) {
return(UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); }}Copy the code
About protected the abstract populated UserDetails retrieveUser implementation, AbstractUserDetailsAuthenticationProvider implementation is DaoAuthenticationProvider.
DaoAuthenticationProvider operations are two main, first retrieve the relevant information from a database, the second is to retrieve the password of user information encryption operation.
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// To retrieve the user, we generally implement the UserDetailsService interface, instead of retrieving the user information from the database to return the security core class UserDetails
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw newInternalAuthenticationServiceException(ex.getMessage(), ex); }}@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
// Determine whether to use password encryption for this point is not deep, you can go to check this knowledge
boolean upgradeEncoding = this.userDetailsPasswordService ! =null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user); }}Copy the code
4.2. Copy the homework
Read the source code, in fact, if we want to rewrite, the main thing to do the following:
-
Public Boolean supports(Class
authentication) method.Sometimes we have multiple AuthenticationProviders that support different Authentication objects, so when a specific AuthenticationProvier is passed inside the ProviderManager, It selects the supported provider from the AuthenticationProvider list to authenticate the corresponding Authentication object
In short, you specify which Authentication object the AuthenticationProvider authenticates. Such as specified UsernamePasswordAuthenticationToken DaoAuthenticationProvider certification,
So we specify EmailCodeAuthenticationToken EmailCodeAuthenticationProvider authentication.
-
Retrieve the database and return a security core class, UserDetail.
-
Create an authenticated Authentication object
Now that we know what to do, we can start looking at the code.
/ * * *@Author: crush
* @Date: 2021-09-08 21:14
* version 1.0
*/
@Slf4j
public class EmailCodeAuthenticationProvider implements AuthenticationProvider {
ITbUserService userService;
public EmailCodeAuthenticationProvider(ITbUserService userService) {
this.userService = userService;
}
/** * Authentication */
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if(! supports(authentication.getClass())) {return null;
}
log.info("EmailCodeAuthentication authentication request: %s", authentication);
EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;
UserDetails user = userService.getByEmail((String) token.getPrincipal());
System.out.println(token.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("Unable to obtain user information");
}
System.out.println(user.getAuthorities());
EmailCodeAuthenticationToken result =
new EmailCodeAuthenticationToken(user, user.getAuthorities());
/* Details contains attributes like IP address, sessionId, etc. */
result.setDetails(token.getDetails());
return result;
}
@Override
public boolean supports(Class
aClass) {
returnEmailCodeAuthenticationToken.class.isAssignableFrom(aClass); }}Copy the code
π¨π» Perform the configuration in the configuration class
Basically do the following things:
- Inject filters and authenticators into
spring
In the - The logon success handler and logon failure handler are injected into
Spring
, or handle login successes and failures in custom filters. - Add to the filter chain
@Bean
public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter(a) {
EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter();
emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
return emailCodeAuthenticationFilter;
}
@Bean
public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider(a) {
return new EmailCodeAuthenticationProvider(userService);
}
/** * Use BCryptPasswordEncoder to encrypt the password, so use BCryptPasswordEncoder to authenticate the password@param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
//authenticationProvider Adds the identity authenticationProvider based on the custom authenticationProvider passed in.Auth. AuthenticationProvider (emailCodeAuthenticationProvider ()); }Copy the code
.and()
.authenticationProvider(emailCodeAuthenticationProvider())
.addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(mobileCodeAuthenticationProvider())
.addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
Copy the code
π§βοΈ test and source code
The specific configuration, startup mode, environment, etc., of the project are explained in details on github and Gitee documents.
The source code contains SQL files, configuration files, and links to related blogs. The source code is also annotated to make it as clear as possible.
To ensure that everyone can run and test correctly to the greatest extent possible.
Source: gitee ws-security
π talk to yourself
If you don’t quite understand the content of this article, you can read my other article first:
SpringBoot integrates Security for Security control and uses Jwt to make tokens.
It should be easier to come back to this article later.
I really want to teach people, not just to scribble, but mostly to get the kind of joy that comes from success, which makes people feel good.
That’s all for today’s article.
Hello, THIS is blogger Ning Zaichun: homepage
If you encounter doubts in the article, please leave a message or private letter, or add the homepage contact information, will reply as soon as possible.
If you find any problems in the article, please correct them. Thank you very much.
If you think it will help you, please click “like” before leaving!