During the summer vacation, I learned Spring Security and successfully applied it to the project. In practice, I found out a set of permissions combined with JSON + JWT (JSON Web Token) + Spring Boot + Spring Security technology while the National Day holiday record.

The source code for all of the following steps is available on my Github. To learn more, read readme.md.

A brief introduction of each technology

Json: Data exchange format that interacts with the front-end

Personally, it can promote the decoupling of the front and back ends of the Web and improve the work efficiency of the team. It is also a tool for interacting with Android and iOS. Currently I don’t think of any other forms of communication other than JSON and XML (maybe I will look at them later when I have free time).

It also features lightweight, concise and clear hierarchies that make it easy to read and write, and reduce server bandwidth usage.

jwt (json web token)

In human words, the user’s identity information (account name), other information (not fixed, according to the need to increase) is extracted when the user logs in, and processed into a string of ciphertext through encryption means, when the user logs in successfully with the return result sent to the user. Each time a user requests the ciphertext, the server parses the ciphertext to determine whether the user has the permission to access related resources and returns the corresponding result.

Some of the benefits are excerpted from the web, and readers interested in more information about JWT can Google it:

  1. Compared to a session, it does not need to be stored on the server and has no server memory overhead.
  2. Stateless and extensible: For example, if three machines (A, B, and C) form A server cluster, if the session is stored on machine A, the session can only be stored on one of the servers, then you cannot access machine B and C, because the session is not stored on machine B and C, and you can use token to verify the validity of user requests. And I can add a few more machines, so that’s what scalability means.
  3. By doing so, you can support cross-domain access.

Spring Boot

Spring Boot is a framework that simplifies the setup and development of Spring applications. It makes you exclaim, “Wocao! What a convenient thing! Mama doesn’t have to worry about me not being able to configure XML configuration files anymore!” .

Spring Security

Spring Security is a Security permission control framework provided by Spring Security. You can customize the role identity and the permissions of the identity according to the needs of users, and complete blacklist operations and intercept unauthorized operations. With Spring Boot, a complete permission system can be developed quickly.

Spring Security execution process in this technical solution

Spring Security execution process in this technical solution

It can be seen from the figure that the execution process revolves around tokens.

The user logs in to get the token we return and saves it locally. After receiving the request from the client, the server will determine whether there is a token. If there is a token, the server will parse the token and write the permission to the session. If there is no request, the step of parsing the token will be skipped directly, and then determine whether the interface accessed this time needs authentication. Whether the corresponding permissions are required and react based on the authentication status in this session.

Start implementing the security framework

Step 1: Set up the project and configure the data source

  1. Create a Spring Boot project using Itellij Idea
Required components

Select Web, Security, Mybatis, and JDBC components.

  1. Create the required database spring_security in the database
Set up a database

  1. Configure the data source in the Spring Boot configuration file application.properties
Configuring a Data Source

  1. Start the project to see if Spring Boot has configured Spring Security for us.
Start the project

If it starts correctly, you can see that Spring Security generates a default password.

When we access localhost:8080, a basic authentication box pops up

Enter the username user password and the automatically generated password will return a pass message (404, because we haven’t created any pages yet). If you enter the wrong username or password, 401 will be returned, indicating that you are not authenticated

If you have reached this point, it means that you have configured the required environment, and it is time to move on to the next step.

Step 2: Generate our JWT

In this step we will learn how to generate our custom tokens according to our needs!

  1. Turn off Spring Boot and Spring Security for us. (Because Spring Security is configured to block our custom login interface by default)

Create the Spring Security configuration class webSecurityconfig.java

Public Class WebSecurityConfig extends Public class WebSecurityConfig extends public class WebSecurityConfig extends public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizerequests ().anyRequest().permitall () // Allow all requests to pass.and().csrf().disable() // disable Spring Security's own cross-domain processing SessionManagement () / / customize our own strategy session. SessionCreationPolicy (sessionCreationPolicy. STATELESS); // Adjust Spring Security to not create and use sessions}}Copy the code
  1. Create the corresponding users and roles in the database.

Create the user table user

The user table

The attributes and functions are as follows:

  • Username: indicates the username
  • Password: password
  • Role_id: indicates the id of the role to which the user belongs
  • Last_password_change: indicates the time when the password was changed last
  • Enable: indicates whether to enable the account. It can be used to blacklist users

Create the role table role

Role table

Each attribute functions as follows:

  • Role_id: indicates the id of a role
  • Role_name: indicates the role name
  • Auth: permission of the role
  1. Write the corresponding login password judgment logic

Since the login function is easy to implement, I won’t write it here.

  1. Write the Token manipulation class (generate the Token part)

Because there are built wheels on the Internet, we can just take them and make some modifications and use them.

Import JWT wheels built online using Maven

<dependency> <groupId> IO. Jsonwebtoken </groupId> <artifactId> JJWT </artifactId> <version>0.4</version> </dependency>Copy the code

Create our own token manipulation class tokenUtils.java

public class TokenUtils {

    private final Logger logger = Logger.getLogger(this.getClass());

    @Value("${token.secret}")
    private String secret;

    @Value("${token.expiration}") private Long expiration; /** * Generate Token from TokenDetail ** @param TokenDetail * @return
     */
    public String generateToken(TokenDetail tokenDetail) {
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("sub", tokenDetail.getUsername());
        claims.put("created", this.generateCurrentDate());
        returnthis.generateToken(claims); } /** * generates tokens from claims ** @param Claims * @return
     */
    private String generateToken(Map<String, Object> claims) {
        try {
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.generateExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, this.secret.getBytes("UTF-8"))
                    .compact();
        } catch (UnsupportedEncodingException ex) {
            //didn't want to have this method throw the exception, would rather log it and sign the token like it was before logger.warn(ex.getMessage()); return Jwts.builder() .setClaims(claims) .setExpiration(this.generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, this.secret) .compact(); } /** * Token expiration time ** @return */ private Date GenerateationDate () {return new Date(System.currentTimemillis () +  this.expiration * 1000); } @return */ private Date generateCurrentDate() {return new Date(system.currentTimemillis ()); }}Copy the code

The utility class currently does the following:

  • Encapsulate the user name in the downloaded wheel’s token body Claims and encapsulate the current time in it.
  • Then calculate the token expiration time and write it into the token of the wheel
  • The token of the wheel is salted and encrypted to generate a string, which is our customized token

The TokenDetail entry parameter to the method that generates the custom token is defined as follows

Public interface TokenDetail {//TODO: this is a layer of encapsulation. The reason why we don't use username directly is that we can add other information to the token in the future. String getUsername(); } public class TokenDetailImpl implements TokenDetail { private final String username; public TokenDetailImpl(String username) { this.username = username; } @Override public StringgetUsername() {
        returnthis.username; }}Copy the code

The utility class also extracts the string used to encrypt the token and the token expiration time into application.properties

# token encryption key
token.secret=secret
# Token expiration time, in seconds, 604800 is one week
token.expiration=604800Copy the code
  1. So far, the tutorial of token generation has been completed. As for the login interface, the operation of determining whether the account password is correct is left to the readers. The readers only need to return the generated token to the user in the result when the login is successful.

Step 3: Verify whether the token is valid, and obtain account details (permissions, whether the account is locked) based on the token

  1. Analyze the implementation process

In step 2, we encapsulate the user’s username, creation time of token, and expiration time of token into the encrypted token string, in order to serve the purpose of verifying user permissions at this time.

Suppose we get a bunch of tokens from the user and want to get the details of the user based on this bunch of tokens:

A. Try to parse the string of tokens. If the token is successfully parsed, go to the next step. Otherwise, the parsing process is terminated. According to the resolved username from the database to find the user account, the last password change time, permission, whether to seal the user details, such as the information encapsulated in an entity class (userDetail class). If the user cannot be found, terminate the parsing process C. Check the sealing status recorded in userDetail. If the account has been sealed, return the result and terminate the request D. Verify that the token is within the validity period according to the userDtail. If the token is not within the validity period, the parsing process is terminated. Otherwise, continue E. Write the user permissions recorded in userDetail to this request session, and the parsing is complete.

Please refer to the following figure for understanding:

Analyze the process of parsing tokens and checking permissions

Let’s start implementing it

  1. Try parsing token to get username
/** * get username ** @param token * @ from tokenreturn
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        returnusername; } /** * Body Claims ** @param token * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(this.secret.getBytes("UTF-8"))
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }Copy the code

In this code, we decrypt the token, get the body claims encapsulated in the token (the wheel someone else made), and then try to get the username string encapsulated inside.

  1. Get the user details, userDetail, from the database

Here we will implement a UserDetailService interface for Spring Security that has only one method, loadUserByUsername. The flow chart is as follows

Get the flowchart for UserDetail

The code is as follows:

public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; /** * get userDetail * @param username * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = this.userMapper.getUserFromDatabase(username);

        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        } else {
            return SecurityModelFactory.create(user);
        }
    }
}

public class User implements LoginDetail, TokenDetail {

    private String username;
    private String password;
    private String authorities;
    private Long lastPasswordChange;
    private char enable; Public class SecurityModelFactory {public static UserDetailImpl create(User User) {public static UserDetailImpl create(User User) { Collection<? extends GrantedAuthority> authorities; try { authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getAuthorities()); } catch (Exception e) { authorities = null; } Date lastPasswordReset = new Date(); lastPasswordReset.setTime(user.getLastPasswordChange());returnnew UserDetailImpl( user.getUsername(), user.getUsername(), user.getPassword(), lastPasswordReset, authorities, user.enable() ); }}Copy the code

The mapper class of the User class is defined as follows:

public interface UserMapper {


    User getUserFromDatabase(@Param("username") String username);

}Copy the code

The corresponding XML file is:

<select id="getUserFromDatabase"  resultMap="getUserFromDatabaseMap">
        SELECT
        `user`.username,
        `user`.`password`,
        `user`.role_id,
        `user`.enable,
        `user`.last_password_change,
        `user`.enable,
        role.auth
        FROM
        `user` ,
        role
        WHERE
        `user`.role_id = role.role_id AND
        `user`.username = #{username}
    </select>

    <resultMap id="getUserFromDatabaseMap" type="cn.ssd.wean2016.springsecurity.model.domain.User">
        <id column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="last_password_change" property="lastPasswordChange"/>
        <result column="auth" property="authorities"/>
        <result column="enable" property="enable"/>
    </resultMap>Copy the code

At this point, we are done getting user details. Then you only need to restrict the access permission of the interface and require users to access the interface with tokens to control the access permission.

Step 4: Define an interceptor that resolves tokens

Old rules, flow chart:

The execution flow of the interceptor that resolves the token

Let’s define the interceptor

Public class AuthenticationTokenFilter extends UsernamePasswordAuthenticationFilter {/ * * * json web token in the request header name * / @Value("${token.header}") private String tokenHeader; /** * @autoWired Private TokenUtils TokenUtils; /** * Spring Security's core operation service class * in the current class will use UserDetailsService to get the userDetails object */ @autoWired private UserDetailsService  userDetailsService; @Override public voiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// Convert ServletRequest to HttpServletRequest to get the token HttpServletRequest = (HttpServletRequest) request; AuthToken = httprequest.getheader (this.tokenheader); // If there is no token or there is an exception when retrieving username, The username is null String username = this. TokenUtils. GetUsernameFromToken (authToken); // If you parse the token successfully and get username, the session permission has not been written yetif(username ! = null && SecurityContextHolder. GetContext () getAuthentication () = = null) {/ / UserDetailsService user from the database The UserDetails class is the entity class used by Spring Security to store user permissions, UserDetails UserDetails = this.userDetailsService.loadUserByUsername(username); // Check whether the token brought by the user is valid. // Check whether the token name is the same as the userDetails, whether the token expires, and whether the generation time of the token is before the last password change. // If the check succeedsif(this.tokenUtils.validateToken(authToken, Populated userDetails)) {/ / generated by certification UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest)); / / to write permissions into the session SecurityContextHolder. GetContext () setAuthentication (authentication); }if(! userDetails.isEnabled()){ response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json; charset=UTF-8");
                response.getWriter().print("{\" code \ ": \" 452 \ "and \" data \ ": \" \ ", \ "message \" : \ "account number in the blacklist \"}");
                return; } } chain.doFilter(request, response); }}Copy the code

The tokenutils.validateToken (authToken, userDetails) method to check whether the token is valid is defined as follows:

/** * check whether the token is valid * @param token * @param userDetails * @return
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        UserDetailImpl user = (UserDetailImpl) userDetails;
        final String username = this.getUsernameFromToken(token);
        final Date created = this.getCreatedDateFromToken(token);
        return(username.equals(user.getUsername()) && ! (this.isTokenExpired(token)) && ! (this.isCreatedBeforeLastPasswordReset(created, user.getLastPasswordReset()))); } /** * get the token creation time we encapsulated in the token * @param token * @return
     */
    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            created = new Date((Long) claims.get("created"));
        } catch (Exception e) {
            created = null;
        }
        returncreated; } /** * get the token expiration time * @param token * @ that we encapsulated in the tokenreturn
     */
    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        returnexpiration; } /** * Checks whether the current time is after the expiration time encapsulated in the token. If so, the token is deemed expired * @param token * @return
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = this.getExpirationDateFromToken(token);
        returnexpiration.before(this.generateCurrentDate()); } /** * Check whether the token was created before the last password change (tokens generated before the password change are considered invalid even if they have not expired) * @param created * @param lastPasswordReset * @return
     */
    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return(lastPasswordReset ! = null && created.before(lastPasswordReset)); }Copy the code

Step 5: Register the interceptor of Step 4 to write the user’s permissions to the session before Spring Security reads the session permissions

Add the following configuration to the SpringSecurity configuration class, webSecurityconfig.java

Public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/ * * * registration token transformation interceptor for bean * if the client came the token, Then the interceptor resolves the token to give the user permissionreturn
     * @throws Exception
     */
    @Bean
    public AuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter();
        authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationTokenFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/auth").authenticated() // Valid tok.antmatchers ("/admin").hasAuthority("admin") // You need to have admin. AntMatchers ("/ADMIN").hasRole("ADMIN"AnyRequest ().permitall () // Allows all requests to pass.and().csrf().disable() // disables Spring Security's cross-domain processing SessionManagement () / / customize our own strategy session. SessionCreationPolicy (sessionCreationPolicy. STATELESS); // Adjust to let Spring Security not create and use sessions /** * The core configuration of this JSON Web token permission control * at the moment before Spring Security starts to determine whether the session is entitled * Resolve tokens by adding filters, All permissions to the user of this Spring Security session * / HTTP addFilterBefore (authenticationTokenFilterBean (), UsernamePasswordAuthenticationFilter.class); }}Copy the code

We register the interceptor defined in Step 4 as a bean in Spring, and register the token resolution by adding filters at the moment before Spring Security starts to judge whether the session has the right limit, and write all user permissions into the session.

Second, we added three Ant style address interception rules:

  • /auth: Requires a valid token
  • /admin: The account that carries the token must have the admin permission
  • /ADMIN: The account that carries a token must have the ROLE_ADMIN identity

Start the program to port 8080, login to the guest account through the /login interface, and try to access the /auth interface. The result is as follows:

Results of access to auth

Obviously, because the token was valid, the interception was successful

Next try to access the /admin interface and the result is as follows:

Result of access to admin

Apparently, the request was intercepted because the token carried did not have admin permission

Now that we have a system of permission rules with simple permissions, in the next step we will optimize the results returned with no permissions and conclude this summary.

Step 6: Refine the 401 and 403 returns

Define the 401 processor to implement the AuthenticationEntryPoint interface

Public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {/ * * * not login or have the right to limit the trigger operation * returns {"code": 401,"message":"Little brother, you are not carrying a token or token is invalid!"."data":""} * @param httpServletRequest * @param httpServletResponse * @param e * @throws IOException * @throws ServletException */ @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {/ / return json error information in the form of httpServletResponse. SetCharacterEncoding ("UTF-8");
      httpServletResponse.setContentType("application/json");

      httpServletResponse.getWriter().println("{\"code\":401,\"message\":\" Little brother, you do not carry token or token is invalid! \",\"data\":\"\"}"); httpServletResponse.getWriter().flush(); }}Copy the code

Define the 403 processor to implement the AccessDeniedHandler interface

@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {/ / return json error information in the form of httpServletResponse. SetCharacterEncoding ("UTF-8");
        httpServletResponse.setContentType("application/json");

        httpServletResponse.getWriter().println("{\"code\ :403,\"message\":\" Little brother, you have no access! \",\"data\":\"\"}"); httpServletResponse.getWriter().flush(); }}Copy the code

Configure these two processors into the configuration class for SpringSecurity:

Public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/ * * * * / registered 401 processor @autowired private EntryPointUnauthorizedHandler unauthorizedHandler; / / @autoWired Private MyAccessDeniedHandler accessDeniedHandler; . @Override protected void configure(HttpSecurity http) throws Exception { http ... / / the treatment. When the configuration is intercepted exceptionHandling () authenticationEntryPoint (enclosing unauthorizedHandler) / / add token is invalid or not carrying token when processing .accessdeniedHandler (this.accessdeniedHandler) // Add an unauthorized handler... }}Copy the code

Try to access the /admin interface as guest with the following result:

Access the admin interface as guest

Hee hee, apparently mission accomplished!! (This interface can also be configured using lamda expressions, which is left to explore.)

Slip away…