Source code repository: github.com/zhshuixian/…

In the previous section “Spring Security (Token) Login and Registration “, we introduced the integration of Spring Boot and Spring Security to implement Token login and authentication. We will implement Spring Boot integration Shiro to implement Token login and authentication.

1) Introduction to Apache Shiro

As mentioned above, Spring Security and Apache Shiro are common Security frameworks for Java development. Shiro is a powerful open source Security framework.

Apache Shiro™** is a powerful and easy-to-use Java security framework for performing authentication, authorization, encryption, and session management. Using Shiro’s easy-to-understand apis, you can quickly and easily secure any application – from the smallest mobile applications to the largest Web and enterprise applications.

To use Shiro, you need to understand its three core concepts:

  • Subject: a security term meaning “currently executing user”
  • SecurityManager: SecurityManager, the core of Shiro, provides various security management services and manages all subjects.
  • Realm: A Realm is a “bridge” or “connector” between an application and secure data. When Shiro needs to interact with secure data (such as user account information) for authentication (login authentication) and authorization (access control), Shiro does so through one or more of its configured realms.

Further reading: shiro.apache.org/architectur…

2) Shiro project configuration

Create a new project, 07-Shiro, remember to check MySQL, MyBatis, Web dependency. For Maven projects, the corresponding dependency configuration is also given at the end of the article.

Gradle project configuration

    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.12.'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter
    compile group: 'org.apache.shiro', name: 'shiro-spring-boot-web-starter', version: '1.52.'
    // https://github.com/jwtk/jjwt
    compile 'io.jsonwebtoken:jjwt-api:0.111.'
    runtime 'io.jsonwebtoken:jjwt-impl:0.111.',
            // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
            / / 'org. Bouncycastle: bcprov - jdk15on: 1.60',
            // or 'IO. Jsonwebtoken :jjwt-gson:0.11.1' for gson
            'io.jsonwebtoken:jjwt-jackson:0.111.'
Copy the code

2.1) Project configuration

Configure the MySQL database and MyBatis camel name conversion, application.properties

You only need to change the database URL, username, password, and JDBC Driver
MySQL 8 needs to specify serverTimezone to connect to MySQLspring.datasource.url=jdbc:mysql://localhost:3306/spring? useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.password=xiaoxian
spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# MyBatis Hump naming conversion
mybatis.configuration.map-underscore-to-camel-case=true
Copy the code

Add @ MapperScan

@MapperScan("org.xian.security.mapper")
public class SecurityApplication {}
Copy the code

3) Start using Shiro

Main structure of the project:

  • Controller package: API interface
  • Service package: Provides interface services for apis
  • Mapper package: MyBatis Mapper class
  • Entity package: Entity class
  • Shiro package: Token interception validation, Token generation, Shiro Realm configuration, etc

MyResponse: public Response returns a message class:

public class MyResponse implements Serializable {
    private static final long serialVersionUID = -2L;
    private String status;
    private String message;
}
Copy the code

2.1) Entity classes Entity and Mapper

The table structure here is the same as in the previous section, with the user table SYS_user

field type note
user_id bigint Since the primary key
username varchar(18) User name, not null unique
password varchar(128) Password, not empty
user_role varchar(8) USER Role (USER/ADMIN)
user_permission varchar(36) User permissions

The USER role is USER/ADMIN. The case that a USER may have multiple roles is not considered.

Shiro can specify permissions, such as Update permissions and Create permissions, allowing for more granular control. The permissions are separated by semicolons (;), for example, update,create, and delete, indicating that the user has the update permissions.

Passwords are encrypted using HASH HASH.

SQL

create table sys_user
(
    user_id         bigint auto_increment,
    username        varchar(18)  not null unique,
    password        varchar(128) not null,
    user_role       varchar(8)   not null,
    user_permission varchar(36)   not null,
    constraint sys_user_pk
        primary key (user_id)
);
Copy the code

Entity Entity class: Create a package with the name Entity. Create a new SysUser class under Entity:

public class SysUser implements Serializable {
    private static final long serialVersionUID = 4522943071576672084L;
    private Long userId;
    private String username;
    private String password;
    private String userRole;
    private String userPermission;
    // omit getter setter constructor
}
Copy the code

Mapper interface class: Create a package Mapper, create a SysUserMapper class:

public interface SysUserMapper {
    /** Insert a record * to sys_user@paramSysUser User information */
    @Insert("Insert Into sys_user(username, password,user_role,user_permission) Values(#{username}, #{password},#{userRole},#{userPermission})")
    @Options(useGeneratedKeys = true, keyProperty = "userId")
    void insert(SysUser sysUser);
    /** Query user information based on Username *@paramUsername username *@returnUser information */
    @Select("Select user_id,username, password,user_role,user_permission From sys_user Where username=#{username}")
    SysUser selectByUsername(String username);
}
Copy the code

2.2) Token configuration

First, implement the function of Token generation and verification:

  • RSA key Public key tool class
  • Token generation and validation tool classes

Create RsaUtils class under shiro package, utility class for RSA public key and key. Note that in JDK 8, 2048-bit keys are not supported. The code refers to Spring Boot 2.X Combat –Spring Security (Token) login and registration section 2.2) Token configuration:

TokenUtils: Utility class that generates and validates tokens. The main part of the optional Token body is that this information is not needed for authentication and authorization. The main code is the same as in the previous section, mainly adding a Refresh function to the Token:

@Component
public class TokenUtils implements Serializable {
    private static final long serialVersionUID = -3L;
    /** How long is the Token valid in seconds **/
    private static final Long EXPIRATION = 2 * 60L;

    /** Generates the Token string setAudience Receiver setExpiration time role User role *@paramSysUser User information *@returnGenerated Token string or NULL */
    public String createToken(SysUser sysUser) {
        try {
            // Expiration time of Token
            Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
            / / Token is generated
            String token = Jwts.builder()
                    // Set Token issuer optional
                    .setIssuer("SpringBoot")
                    // Set the Token receiver based on the user name
                    .setAudience(sysUser.getUsername())
                    // Set the expiration time
                    .setExpiration(expirationDate)
                    // Setting the Token generation time Optional
                    .setIssuedAt(new Date())
                    // Use the claim method to set a key = role, value = userRole value
                    .claim("role", sysUser.getUserRole())
                    // User role
                    // Set a key = permission, value = permission value via the claim method
                    .claim("permission", sysUser.getUserPermission())
                    // Set the encryption key and algorithm. Use the private key for encryption and ensure that the private key is not leaked
                    .signWith(RsaUtils.getPrivateKey(), SignatureAlgorithm.RS256)
                    .compact();
            return String.format("Bearer %s", token);
        } catch (Exception e) {
            return null; }}/** Verify the Token and obtain the user name and user permission information *@paramToken Token character string *@returnSysUser User information */
    public SysUser validationToken(String token) {
        try {
            // Decrypts the Token to obtain the Claims body
            Claims claims = Jwts.parserBuilder()
                    // Set public key decryption, assuming that the private key is private, so that the Token can only be generated by itself, thus verifying the Token
                    .setSigningKey(RsaUtils.getPublicKey())
                    .build().parseClaimsJws(token).getBody();
            assertclaims ! =null;
            SysUser sysUser = new SysUser();
             // Get user information
            sysUser.setUsername(claims.getAudience());
            sysUser.setUserRole(claims.get("role").toString());
            sysUser.setUserPermission(claims.get("permission").toString());
            return sysUser;
        } catch (Exception e) {
            return null; }}}Copy the code

The Token refresh section is covered separately later.

2.3) Shiro configuration

Implement Shiro’s Realm, interceptors, etc

  • ShiroAuthToken: Implements the AuthenticationToken interface
  • ShiroRealm: Customize realms, verify tokens, and obtain user roles and permissions from tokens
  • ShiroAuthFilter: Interceptor that intercepts all requests and validates tokens
  • ShiroConfig: Shiro configuration to configure realms, interceptors, and so on into SecurityManager

ShiroAuthToken: Implements the AuthenticationToken interface as a carrier for tokens passed into a Realm:

public class ShiroAuthToken implements AuthenticationToken {
    private String token;
    public ShiroAuthToken(String token) { this.token = token; }
    
    @Override
    public Object getPrincipal(a) { return token;  }

    @Override
    public Object getCredentials(a) { returntoken; }}Copy the code

ShiroRealm: Obtains tokens from ShiroAuthToken and authenticates and configures role permissions.

@Service
public class ShiroRealm extends AuthorizingRealm {
    @Resource
    TokenUtils tokenUtils;

    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        // Specify the instance where the current authenticationToken needs to be ShiroAuthToken
        return authenticationToken instanceof ShiroAuthToken;
    }
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        ShiroAuthToken shiroAuthToken = (ShiroAuthToken) authenticationToken;
        String token = (String) shiroAuthToken.getCredentials();
        / / authentication Token
        SysUser sysUser = tokenUtils.validationToken(token);
        if (sysUser == null || sysUser.getUsername() == null || sysUser.getUserRole() == null) {
            throw new AuthenticationException("The Token is invalid");
        }
        return new SimpleAuthenticationInfo(token,
                token, "ShiroRealm");
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // Get user information
        SysUser sysUser = tokenUtils.validationToken(principals.toString());
        // Create an authorization object
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // Check whether the user role exists
        if(! sysUser.getUserRole().isEmpty()) {// Set the role
            info.addRole(sysUser.getUserRole());
        }
        if(! sysUser.getUserPermission().isEmpty()) {// Set permissions according to, split
            Arrays.stream(sysUser.getUserPermission().split(",")).forEach(info::addStringPermission);
        }
        returninfo; }}Copy the code

Code parsing:

ShiroRealm inherits AuthorizingRealm and must override both the doGetAuthenticationInfo and doGetAuthorizationInfo methods. Specify that the authenticationToken must be an instance of the ShiroAuthToken we just defined by overriding the SUPPORTS method.

The doGetAuthenticationInfo method mainly obtains tokens from authenticationToken and performs Token authentication and user authorization.

The method of doGetAuthorizationInfo is mainly to implement the configuration of user roles and user permissions. For the system without user roles and permissions, it can not be implemented and directly super.

Implement Token interceptor.

ShiroAuthFilter: Shiro’s interceptor that intercepts and validates Token validity

public class ShiroAuthFilter extends BasicHttpAuthenticationFilter {

    /** */ store H Headers Key for Token */
    protected static final String AUTHORIZATION_HEADER = "Authorization";

    /** ** Token */
    protected static final String BEARER = "Bearer ";

    private String token;


    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        // Set the theme
        // Automatically call ShiroRealm for Token checking
        this.getSubject(request, response).login(new ShiroAuthToken(this.token));
        return true;
    }

    /** Whether to allow access to *@param request     Request
     * @param response    Response
     * @param mappedValue mapperValue
     * @returnTrue: allows playback */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // The Request contains a Token
        if (this.getAuthzHeader(request) ! =null) {
            try {
                executeLogin(request, response);
                // refreshToken 1, the Token is not expired, and refreshToken is called each time to determine whether the Token needs to be refreshed
                TokenUtils tokenUtils = new TokenUtils();
                String refreshToken = tokenUtils.refreshToken(this.token);
                if(refreshToken ! =null) {
                    this.token = refreshToken;
                    shiroAuthResponse(response, true);
                }
                return true;
            } catch (Exception e) {
                // Refresh Token 2. If the Token has expired, refresh the Token within the specified time
                TokenUtils tokenUtils = new TokenUtils();
                String refreshToken = tokenUtils.refreshToken(this.token);
                if(refreshToken ! =null) {
                    this.token = refreshToken.substring(BEARER.length());
                    // re-invoke the executeLogin authorization
                    executeLogin(request, response);
                    shiroAuthResponse(response, true);
                    return true;
                } else {
                    // The Token refresh failed or the Token is invalid
                    shiroAuthResponse(response, false);
                    return false; }}}else {
            // Token does not exist. Unauthorized information is returned
            shiroAuthResponse(response, false);
            return false; }}/** Token preprocessing, obtain the Token * from the Request Header@param request ServletRequest
     * @return token or null
     */
    @Override
    protected String getAuthzHeader(ServletRequest request) {
        try {
            // header Indicates whether the Token exists
            HttpServletRequest httpRequest = WebUtils.toHttp(request);
            this.token = httpRequest.getHeader(AUTHORIZATION_HEADER).substring(BEARER.length());
            return this.token;
        } catch (Exception e) {
            return null; }}/** Unauthorized access or Header add Token *@param response Response
     * @paramRefresh Whether to refresh the Token */
    private void shiroAuthResponse(ServletResponse response, boolean refresh) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        if (refresh) {
            // Refresh the Token and set the returned header
            httpServletResponse.setStatus(HttpServletResponse.SC_OK);
            httpServletResponse.setHeader("Access-Control-Expose-Headers"."Authorization");
            httpServletResponse.addHeader(AUTHORIZATION_HEADER, BEARER + this.token);
        } else {
            // Set the HTTP status code to 401
            httpServletResponse.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
            // Set the Json format to return
            httpServletResponse.setContentType("application/json; charset=UTF-8");
            try {
                // PrintWriter output Response Returns information
                PrintWriter writer = httpServletResponse.getWriter();
                ObjectMapper mapper = new ObjectMapper();
                MyResponse myResponse = new MyResponse("error"."Unauthorized access");
                // Output the object in JSON format. This can be done by overriding the toString() of MyResponse directly through myResponse.toString()
                writer.write(mapper.writeValueAsString(myResponse));
            } catch (IOException e) {
                // Prints logs}}}}Copy the code

Token refresh policy: Currently, Xiaoxian has the following Token refresh policies

  • The back-end provides an interface for refreshing tokens. The front-end accesses the interface based on the Token expiration time cached by the browser. If the Token expires in less than one day, the interface is used to refresh tokens. Front-end implementation None. Back-end implementation refer to some interfaces for user login and Token refresh code
  • The back end determines that the Token is about to expire and refreshes the Token and puts the Header in Response. See the code for details// Refresh Token 1, the disadvantage is that you have to decide whether to refresh the token each time
  • After the Token expires, if the Token expires within the specified time range, the backend can refresh the Token and put the new Token into the Header of Response. See the code for details// Refresh Token 2The disadvantage is that you have to manually determine whether the Token is valid
  • Other, have not thought of, welcome your message

TokenUtils: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO: TODO Counterfeit tokens can be obtained by means of method 3. Interested readers can try it out for themselves.

/** Token refresh *@paramToken token *@returnString New Token or NULL */
public String refreshToken(String token) {
    try {
        // Decrypts the Token to obtain the Claims body
        Claims claims = Jwts.parserBuilder()
                // Set public key decryption, assuming that the private key is private, so that the Token can only be generated by itself, thus verifying the Token
                .setSigningKey(RsaUtils.getPublicKey())
                .build().parseClaimsJws(token).getBody();
        // Refresh Token 1 The following code is unexpired refresh
        // You can change the code to determine whether to refresh the Token directly while validating the Token
        assertclaims ! =null;
        // Token expiration time
        Date expiration = claims.getExpiration();
        // If the Token expires within 1 minute, refresh the Token
        if(! expiration.before(new Date(System.currentTimeMillis() + 60 * 1000))) {
            // Do not refresh
            return null;
        }
        SysUser sysUser = new SysUser();
        sysUser.setUsername(claims.getAudience());
        sysUser.setUserRole(claims.get("role").toString());
        sysUser.setUserPermission(claims.get("permission").toString());
        // Generate a new Token
        return createToken(sysUser);
    } catch (ExpiredJwtException e) {
        // Refresh Token 2: The Token will automatically determine whether it is expired during decryption
        // ExpiredJwtException can obtain claims from LLDB Claims()
        // Do not use this directly in practice
        // TODO needs to validate the Token itself using the RSA algorithm
        try {
            Claims claims = e.getClaims();
            // If claims is not empty, the Token has resolved the subject part properly
            assertclaims ! =null;
            // Token expiration time
            Date expiration = claims.getExpiration();
            // If the expiration time is less than 10 minutes, refresh the Token
            if(! expiration.after(new Date(System.currentTimeMillis() - 10 * 60 * 1000))) {
                // More than 10 minutes, no help
                return null;
            } else {
                SysUser sysUser = new SysUser();
                sysUser.setUsername(claims.getAudience());
                sysUser.setUserRole(claims.get("role").toString());
                sysUser.setUserPermission(claims.get("permission").toString());
                returncreateToken(sysUser); }}catch (Exception e1) {
            return null; }}}Copy the code

ShiroConfig: Configure Shiro’s Realm and interceptor, interception rules, and close sessions. For Shiro, beans named securityManager and shiroFilterFactoryBean must be configured. How Shiro’s Starter should be configured is under investigation. Do not add the following error:

required a bean named 'shiroFilterFactoryBean' that could not be found.
Copy the code

ShiroConfig code:

@Configuration
public class ShiroConfig {
    /** Use custom realms and close Session manager *@paramRealm Custom realm *@return SecurityManager
     */
    @Bean
    public DefaultWebSecurityManager securityManager(ShiroRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // Use your own realm
        manager.setRealm(realm);
        / / close the Session
        / / shiro ini ways refer to http://shiro.apache.org/session-management.html#disabling-subject-state-session-storage
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    /** Add interceptors and configure interception rules *@paramSecurityManager securityManager *@returnInterceptors and interception rules */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>(2);
        // Add interceptors for shiroAuthFilter, do not use Spring to manage beans
        filters.put("authFilter".new ShiroAuthFilter());
        factoryBean.setFilters(filters);
        // Always use LinkedHashMap, HashMap does not necessarily follow put order, intercept matching rules from the top down
        // If/API /user/login is already matched, the anon interceptor will not match /**
        // Anon supports anonymous access interceptors
        LinkedHashMap<String, String> filterChainDefinitions = new LinkedHashMap<>(4);
        // Release the login interface and registration
        filterChainDefinitions.put("/api/user/login"."anon");
        filterChainDefinitions.put("/api/user/register"."anon");
        // Other requests go through the custom authFilter
        filterChainDefinitions.put("/ * *"."authFilter");
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitions);
        returnfactoryBean; }}Copy the code

2.4) Login and registration interface

SysUserService: API interface service layer

@Service
public class SysUserService {
    /** Hash the encrypted salt **/
    private final String SALT = "#4d1*dlmmddewd@34%";
    @Resource private TokenUtils tokenUtils;
    @Resource private SysUserMapper sysUserMapper;

    /** User login **/
    public MyResponse login(SysUser sysUser) {
        // Query user information from the database
        SysUser user = sysUserMapper.selectByUsername(sysUser.getUsername());
        if (user == null || user.getUsername() == null || user.getPassword() == null
                || user.getUserRole() == null || user.getUserPermission() == null) {
            return new MyResponse("error"."User information does not exist");
        }
        String password = new SimpleHash("SHA-512", sysUser.getPassword(), this.SALT).toString();
        if(! password.equals(user.getPassword())) {return new MyResponse("error"."Password error");
        }
        / / Token is generated
        return new MyResponse("SUCCESS",
                tokenUtils.createToken(user));
    }

    /** User registration *@paramSysUser User registration information *@returnUser registration result */
    public MyResponse save(SysUser sysUser) throws DataAccessException {
        try {
            // Password encrypted storage
            String password = new SimpleHash("SHA-512", sysUser.getPassword(), this.SALT).toString();
            sysUserMapper.insert(sysUser);
        } catch (DataAccessException e) {
            return new MyResponse("ERROR"."The username or nickname already exists, or the user permissions are incorrect.");
        }
        return new MyResponse("SUCCESS"."User added successfully"); }}Copy the code

The logon logic is not implemented using Shiro’s Realm, and the password store uses sha-512 to encrypt the username store. The Shiro password service feature is still being explored.

SysUserController: API login and registration interface

@RestController
@RequestMapping("/api/user")
public class SysUserController {
    /** Store H Headers Key for Token **/
    protected static final String AUTHORIZATION_HEADER = "Authorization";
    @Resource SysUserService sysUserService;

    /** User login interface *@paramSysUser User name and password *@returnUser Token and role */
    @PostMapping(value = "/login")
    public MyResponse login(@RequestBody final SysUser sysUser, ServletResponse response) {
        MyResponse myResponse = sysUserService.login(sysUser);
        // If the login succeeds
        // Write the Token to the Response Header so that the front end can refresh the Token value from the Header
        if ("SUCCESS".equals(myResponse.getStatus())) {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setStatus(HttpServletResponse.SC_OK);
            httpServletResponse.addHeader(AUTHORIZATION_HEADER, myResponse.getMessage());
        }
        return myResponse;
    }

    @PostMapping("/register")
    public MyResponse register(@RequestBody SysUser sysUser) {
        return sysUserService.save(sysUser);
    }
    
    @GetMapping("/hello")
    public String hello(a) {
        return "Visible to users already logged in"; }}Copy the code

Run the project, access the API for registration and login, and register JSON reference:

{
    "username": "user"."password": "spring"."userRole": "USER"."userPermission":"writer,read"
}
Copy the code

To facilitate testing, the Token validity period is set to 2 minutes. For tokens whose expiration time is less than 1 minute or 10 minutes, the/API /user/ Hello interface returns a new Token in the Response Header.

2.5) Shiro’s permissions and roles

SysUserController: API login and registration interfaces Add the following interfaces:

@RequiresRoles("ADMIN")
@PostMapping("/admin")
public String admin(a) {
    return "The user role of Admin can be seen.";
}

@RequiresPermissions("update")
@GetMapping("/permission")
public String permission(a) {
    return "Update permission required to access";
}
Copy the code

Shiro roles and permissions are set in ShiroRealm’s doGetAuthorizationInfo method.

Re-run the project and access the admin and Permission interfaces as users with different roles and permissions.

summary

In this chapter, the main implementation of Spring Boot integration Shiro Token login and verification, as well as role and permission access control. The following articles are arranged as follows:

  • Wechat scan code login
  • Spring Boot exception interception: Intercepts and encapsulates exception information and returns it to the front end.

Appendix: Maven project configuration

1<! -- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
2<! Add following dependencies -->
3<dependency>
4    <groupId>org.apache.shiro</groupId>
5    <artifactId>shiro-spring-boot-web-starter</artifactId>
6    <version>1.5.2</version>
7</dependency>
Copy the code