On paper come zhongjue shallow, realize that this matter to practice.
wedge
For those of you who know a little about SpringSecurity or have run a simple demo but don’t understand the whole process, this is also a good introduction for those who are interested in SpringSecurity. The sample code is also well annotated. When we do the system, generally do the first module is the authentication and authorization module, because this is a system entrance, is also the most important and basic part of a system, in the authentication and authorization service design set up, the rest of the module to secure access. Shiro and Spring Security are common frameworks for authentication and authorization in the market, but most companies choose to develop their own. Since I’ve seen a lot of Spring Security tutorials before, but I don’t think they are very good, so I came up with the idea of sharing them when I was working on Spring Security these two days, hoping to help people who are interested.
Spring Security framework we mainly use it to solve an authentication and authorization function, so my article will be divided into two parts:
- Part I Certification (This section)
- Part II Authorization (in next article)
I will use a Spring Security + JWT + cache demo for you to show what I want to talk about, after all, the brain of things to reflect in concrete things can be more intuitive for you to understand to recognize. When learning a new thing, I recommend using the top-down learning method, so that you can better understand the new thing, rather than the blind men feeling the elephant.
Note: Only user authentication and authorization are involved and third-party authorization such as OAuth2 is not involved.
1. Workflow of 📖SpringSecurity
To get started with Spring Security, it’s important to understand how it works, because unlike a toolkit, you have to understand it and then customize its usage.
Let’s start by looking at how it works: The official Spring Security documentation reads:
Spring Security’s web infrastructure is based entirely on standard servlet filters.
The Web foundation of Spring Security is Filters.
Spring Security is designed to process Web requests through layers of Filters.
Put it in real Spring Security, and put it in words like this:
A Web request passes through a filter chain. During the process of passing through the filter chain, authentication and authorization are completed. If the request is found to be unauthenticated or unauthorized, exceptions will be thrown according to the permissions of the protected API, and the exception handler will handle these exceptions.
If you use a picture, you can draw it like this. This is a picture I found on Baidu:
As shown in the figure above, a request requesting access to the API passes through the filters in the blue wireframe from left to right, where the green filter is for authentication, the blue filter is for exception handling, and the orange filter is for authorization.
The two green filters in the figure are not going to be mentioned today, because they are the two filters built in Spring Security for form authentication and Basic authentication, and our demo is JWT authentication, so they are not used.
If you haveSpring Security
You should know that there are two names in the configurationformLogin
andhttpBasic
If you turn on both of these items in the configuration, the above filters are turned on.
formLogin
Corresponds to your form form authentication way, namely UsernamePasswordAuthenticationFilter.httpBasic
Corresponds to the Basic authentication way, namely BasicAuthenticationFilter.
In other words, you need to configure these two authentication methods to add them to the filter chain, otherwise they will not be added to the filter chain.
Because Spring Security does not provide JWT authentication in its own filter, we will write a JWT authentication filter in the demo, and then put it in the green position for authentication.
2. 📝 Important concepts of SpringSecurity
Now that we know the general workflow of Spring Security, we need to know some very important concepts or components:
- SecurityContext: context object,
Authentication
Objects are going to go inside. - SecurityContextHolder: Static utility class used to get the context object.
- Authentication: indicates the Authentication interface, which defines the data form of the Authentication object.
- AuthenticationManager: Used for verification
Authentication
, return a completed authenticationAuthentication
Object.
1.SecurityContext
Context object, where authenticated data is stored. The interface is defined as follows:
public interface SecurityContext extends Serializable {
// Obtain the Authentication object
Authentication getAuthentication(a);
// Insert the Authentication object
void setAuthentication(Authentication authentication); } Copy the code
There are only two methods in this interface. The main function is to get or set Authentication.
2. SecurityContextHolder
public class SecurityContextHolder {
public static void clearContext(a) {
strategy.clearContext();
}
public static SecurityContext getContext(a) { return strategy.getContext(); } public static void setContext(SecurityContext context) { strategy.setContext(context); } } Copy the code
It’s a utility class for SecurityContext, which is used to get or set or clear SecurityContext, and by default it stores all the data in the current thread.
3. Authentication
public interface Authentication extends Principal.Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials(a);
Object getDetails(a);
Object getPrincipal(a); boolean isAuthenticated(a); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; } Copy the code
The results of these methods are as follows:
- GetAuthorities: orities get user permissions. Typically, they get the user’s role information.
- GetCredentials: Obtains the information that proves user authentication. Usually, the information such as the password is obtained.
- GetDetails: Get additional information about the user (this information can be in our user table).
- GetPrincipal: Obtains the user identity information. In the case of no authentication, the user name is obtained; in the case of authentication, the UserDetails are obtained.
isAuthenticated
: Get the currentAuthentication
Whether the system is authenticated.setAuthenticated
: Set the currentAuthentication
Authenticated or not (true or False).
Authentication simply defines what a data form should look like for data that has been authenticated by SpringSecurity, with permissions, passwords, identity information, and additional information.
4. AuthenticationManager
public interface AuthenticationManager {
// Authentication method
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Copy the code
AuthenticationManager defines an Authentication method that passes in an unauthenticated Authentication and returns an authenticated Authentication. The default implementation class is ProviderManager. Next, you can imagine how these four parts can be connected together to form the Spring Security authentication process: 1. 👉 first a request comes in with the identity information 2. 👉 is authenticated by the AuthenticationManager, 3. 4. 👉 finally puts the authenticated information into the SecurityContext.
3. 📃 Code preparations
Before we really start to talk about our authentication code, we first need to import the necessary dependencies. Database-related dependencies can choose what JDBC framework they want. Here I use Myabtis-Plus, the secondary development of Chinese people.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> The < version > 0.9.0 < / version > </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> The < version > 3.3.0 < / version > </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> The < version > 5.1.47 < / version > </dependency> Copy the code
Next, we need to define the required components. Since my spring-boot is 2.x, we have to define our own encryptor:
1. Define the crypto Bean
@Bean
public PasswordEncoder passwordEncoder(a) {
return new BCryptPasswordEncoder();
}
Copy the code
This Bean is not necessary, Spring Security will use the cryptographer we defined for authentication operations, and an exception will occur if it does not.
2. Define the AuthenticationManager
@Bean
public AuthenticationManager authenticationManager(a) throws Exception {
return super.authenticationManager();
}
Copy the code
We declare Spring Security’s authenticationManager as a Bean. We declare Spring Security’s authenticationManager as a Bean. We declare Spring Security’s authenticationManager as a Bean to authenticate us.
3. Implement UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleInfoService roleInfoService;
@Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { log.debug("Start login authentication, username: {}",s); // Authenticate the user by user name QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.lambda().eq(UserInfo::getLoginAccount,s); UserInfo userInfo = userService.getOne(queryWrapper); if (userInfo == null) { throw new UsernameNotFoundException("User name does not exist, login failed."); } // Build the UserDetail object UserDetail userDetail = new UserDetail(); userDetail.setUserInfo(userInfo); List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId()); userDetail.setRoleInfoList(roleInfoList); return userDetail; } } Copy the code
Implement the UserDetailsService abstract method and return a UserDetails object. During authentication, SpringSecurity will call this method to access the database and search for the user. The logic can be customized, either from the database or from the cache. But we need to assemble the user information and permission information we queried into a UserDetails return.
UserDetails is also an interface that defines the data form, which is used to save the data we find out from the database. Its function is mainly to verify the account status and obtain the permission, and the specific implementation can refer to the code of my warehouse.
4. TokenUtil
Since we are JWT authentication mode, we also need a utility class to help us manipulate tokens. Generally speaking, it has the following three methods:
- Create a token
- Authentication token
- Anti-parse the information in the token
In my code below, the JwtProvider acts as a Token utility class, which can be implemented in my repository code.
✍ code concrete implementation
After the previous explanation, you should know that using SpringSecurity for JWT authentication requires you to write a filter to do the JWT validation, and then put the filter in the green section. Before we can write this filter, we need to perform an authentication operation, because we need to access the authentication interface to get the token before we can put the token on the request header for the next request. If you don’t understand that, don’t worry, just keep reading and I’ll sort it out again at the end of the video.
1. Authentication method
When accessing a system, the authentication method is usually the first to be accessed. Here I have written down the simplest authentication steps, because in the real system we have to write the login record, the front door password decrypt and so on.
@Override
public ApiResult login(String loginAccount, String password) {
Create UsernamePasswordAuthenticationToken / / 1
UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
/ / 2 certification
Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication); // 3 Save the authentication information SecurityContextHolder.getContext().setAuthentication(authentication); // 4 Generate a custom token UserDetail userDetail = (UserDetail) authentication.getPrincipal(); AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal()); // 5 Put into the cache caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail); return ApiResult.ok(accessToken); } Copy the code
There are five steps, and only the first four are unfamiliar:
- Passing in the user name and password creates a
UsernamePasswordAuthenticationToken
Object, that’s what we talked about earlierAuthentication
With the user name and password as the construction parameters, this object is the unauthenticated object that we createdAuthentication
Object. - Using the Bean we declared earlier –
authenticationManager
Call it theauthenticate
Method for authentication and returns an authenticated oneAuthentication
Object. - When the authentication is complete and there is no exception, it will go to the third step and use
SecurityContextHolder
To obtainSecurityContext
After the certification will be completedAuthentication
Object to put in the context object. - from
Authentication
To get ourUserDetails
The object, we talked about earlier, certifiedAuthentication
Object that calls itgetPrincipal()
Method that we assembled from our previous database queryUserDetails
Object, and create the token. - the
UserDetails
Objects are placed in the cache for later filters.
So even if finished, feeling is very simple, because certification operation will be dominated by the authenticationManager. The authenticate () to help us to complete.
Let’s take a look at the source code to see how Spring Security does this for us (omitted) :
// AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication){
// Check whether the user name is in the Authentication object that is not authenticated
String username = (authentication.getPrincipal() == null)?"NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // Select * from cache where user name is XXX UserDetails user = this.userCache.getUserFromCache(username); // If not, go to this method if (user == null) { cacheWasUsed = false; try { // Call our loadUserByUsername method which overrides the UserDetailsService // Get the UserDetails object we assembled ourselves user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials". "Bad credentials")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { // Verify whether the account is disabled preAuthenticationChecks.check(user); // Verify that the password found by the database is consistent with the password we passed in additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } } Copy the code
After looking at the source code, you will find that as we usually write, the main logic is to check the database and then compare the password.
After login, the effect is as follows:
After we return the token, the next time we request another API, we need to include the token in the request header, according to the JWT standard.
JWT filter
After we have the token, we need to put the filter in the filter chain to resolve the token. Since we do not have a session, we will resolve the current user based on the token in the request every time we try to identify which user the request is from. So we need a filter to intercept all requests, as we also said that the filter we will be in the green part is used to replace UsernamePasswordAuthenticationFilter, so we create a new JwtAuthenticationTokenFilter, Then register it as a Bean and add this when writing the configuration file:
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(a) {
return new JwtAuthenticationTokenFilter();
}
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); } Copy the code
AddFilterBefore semantics is to add a Filter to the XXXFilter before, here is to put the JwtAuthenticationTokenFilter UsernamePasswordAuthenticationFilter before, Because filter execution is also sequential, we must put our filter in the green part of the filter chain to play the effect of automatic authentication. Then we can see the specific implementation JwtAuthenticationTokenFilter:
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain chain) throws ServletException, IOException {
log.info("The JWT filter validates the request header token for automatic login...");
// Obtain the information in the Authorization request header String authToken = jwtProvider.getToken(request); // Check if the content is false and if it is a false start if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) { // Bearer removes the token prefix and obtains real tokens authToken = authToken.substring(jwtProperties.getTokenPrefix().length()); // Get the login account in the token String loginAccount = jwtProvider.getSubjectFromToken(authToken); if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) { // There is no need to log in again. UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class); // After receiving the user information, verify the user information and token if(userDetails ! =null && jwtProvider.validateToken(authToken, userDetails)) { // Assemble the authentication object. The construction parameters are Principal Credentials and Authorities // The grant Authorities method is used in the following interceptors UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); // Put the authentication information into the context object SecurityContextHolder.getContext().setAuthentication(authentication); log.info("JWT filter automatically logon successfully by verifying request header token, user: {}", userDetails.getUsername()); } } } chain.doFilter(request, response); } Copy the code
Although the steps in the code are very detailed, but probably because the code is too long to read, I will simply talk about it, or you can directly go to the warehouse to check the source code:
- get
Authorization
Token information corresponding to the request header - Removal of token head (Bearer)
- Parse the token and get the login account we put in it
- Since we’ve logged in before, we’ll take ours directly from the cache
UserDetail
Information can be - Check whether the UserDetail is null and whether the token has expired.
UserDetail
Whether the user name and token are consistent. - Assemble a
authentication
Object, put it in the context object, so that the filters that follow see that we have it in the context objectauthentication
Object, it’s like we’ve already certified it.
This way, every request with the correct token will be found and put into the context object. We can use SecurityContextHolder to get the Authentication object in the context object easily.
When we’re done, we start our demo, and we can see the following filters in the filter chain, of which our custom is the fifth one:
🐱🏍 : The account information and role information we get after logging in will be put into the cache. When the request with token comes, we will take it out of the cache and put it into the context object again.
Combined with the authentication method, our logical chain becomes:
Log in to 👉 and get token👉. Request to carry token👉 with JWT filter, intercept 👉, verify token👉. Put the object found in the cache into the context
After that, our logic for authentication is complete.
4. 💡 code optimization
With authentication and JWT filters complete, the JWT project is actually ready to run and achieve the desired effect. If we want to make the program more robust, we need to add some accessibility features to make the code more friendly.
1. Failed to authenticate the processor
This handler is triggered when the user is not logged in or the token resolution fails, returning an invalid access result.
2. Processor permissions are insufficient
When the user’s own permissions do not meet the requirements for access to the API, this handler is triggered, returning a result of insufficient permissions.
3. Exit method
User exit is usually a matter of clearing the context object and the cache, or you can do an add-on, which is required.
4. The token refresh
JWT project token refresh is also necessary. The main method of refreshing the token is put in the token tool class. After refreshing, you need to reload the cache again.
Afterword.
Spring Security is a bit of a challenge to get started. When I first learned about it, I saw a tutorial from Silicon Valley. The lecturer in that video combined it with Thymeleaf, which led to many blogs on the Internet talking about Spring Security in this way. Instead of paying attention to the anterior and posterior separation. Also have do tutorial is directly inherited UsernamePasswordAuthenticationFilter filter, this method is feasible, but we know the whole operation process after you know it is not necessary to do so, don’t need to carry on XXX, Just write a filter and put it there.
This article code: code cloud address GitHub address