This is the sixth day of my participation in the First Challenge 2022. For details: First Challenge 2022.

# SpringBoot integration shiro — Front and back end separation mode

In the last article I briefly described the workflow of Realm, followed by ShiroConfig and the implementation of the front and back end separation. In this mode, the interface is called by the front end and the return value is obtained. However, the traditional reconfiguration of shiroConfig will have this statement:

shiroFilterFactoryBean.setLoginUrl("/login");
Copy the code

This is used to set the login page, we will set the filter when configuring shiroConfig, I read a lot of related articles are directly used shiro some filters, but these built-in filters will redirect to the login page in the case of authentication failure, can not meet our needs, So shiro’s filter can be rewritten to meet our own needs. If the authentication fails, it will throw an exception and then set the exception capture to encapsulate the exception information and return the status code to the front end to solve the problem of separating the front end and the back end. If you fail to log in, you add the error message and the error code back to the front end and the front end takes care of the rest. Let’s take a look at shiroConfig configuration. The source code can be found at GitHub -> repository address, and the first step is to add the realm validation we defined ourselves in shiro in the previous article

@bean public UserRealm myShiroRealm() {UserRealm UserRealm = new UserRealm(); return userRealm; }Copy the code

And then there’s the crucial filtering part

/ / the url filtering @ Bean public ShiroFilterFactoryBean ShiroFilterFactoryBean (DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //oauth filters <String, Filter> filters = new HashMap<>(); // Oauth filters <String, Filter> filters = new HashMap<>(); filters.put("auth", new AuthFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/druid/**", "anon"); filterMap.put("/sysuser/**", "anon"); Filtermap. put("/swagger/**", "anon"); filterMap.put("/v3/api-docs", "anon"); filterMap.put("/swagger-ui/**", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/**", "auth"); / / login / / shiroFilterFactoryBean setLoginUrl (" sysuser/login "); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); return shiroFilterFactoryBean; }Copy the code

The main thing is to set filters on the interface. Shiro’s built-in filters are commonly used

  • Anon: Access without authentication (login)
  • Auth: Authentication is required for access
  • User: If you use rememberMe
  • Perms: This resource can be accessed only if it has resource permissions
  • Role: The resource can be accessed only after the role permission is granted

As you can see, I have set some interface documents and interfaces related to registration and login to ANon, which means no authentication is required, and the rest go through auth, which means the rest of the interfaces are authenticated by this filter, so we need to rewrite this filter. Let’s rewrite the createToken method:

@Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse Response) throws Exception {// Obtain the request Token (dataId) String Token = TokenUtil.getRequestToken((HttpServletRequest) request); return new AuthToken(token); }Copy the code

In my project, the token corresponds to the user one by one. When the user logs in, the token and expiration time are generated, and then when the user makes a request, the user can know who is accessing the interface or bring the token in the request. After receiving the token, the user can return a token object. AuthToken is the simplest object that can manipulate tokens

public class AuthToken extends UsernamePasswordToken { private String token; public AuthToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; }}Copy the code

AuthToken is inherited from UsernamePasswordToken, we can take a look at the official authentication process

We can see that we started with subjection.login (), regardless of where the subject came from, because Shiro will fetch the subject itself. I’ve seen a lot of articles where you manually get a subject in the login section and then create a UsernamePasswordToken and put the account password in and then execute subject.login(UsernamePasswordToken), you can skip this step, Just make sure our self-configured Realm passes user verification. I will write an article analyzing the whole log-in process. So the token in this realm is the information we use to verify the user’s identity. The token in this realm is the token sent from the front end. Because the token corresponds to the user one by one, it represents the user. So we override createToken to return our own token to Shiro for verification. Then we now reject all access requests in the filter

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
        return true;
    }
    return false;
}
Copy the code

To deny access, the onAccessDenied method is called, and we override the onAccessDenied method

@Override protected boolean onAccessDenied(ServletRequest request, ServletResponse Response) throws Exception {// Obtain the request token, if the token does not exist, Direct return String token = TokenUtil. GetRequestToken ((it) request); If (stringutils.isblank (token)) {RuntimeException RuntimeException = new RuntimeException(" Token does not exist "); // Catch an exception and send it to expiredJwtException Request.setAttribute ("errMsg", runtimeException); / / will be distributed exception to request/filterException controller. The getRequestDispatcher ("/filterException "). The forward (request, response); return false; } return executeLogin(request, response); }Copy the code

The token does not exist. If the token does not exist, the user has not logged in and has not received the token. Therefore, the user can directly throw the exception. See the article # SpringBoot how to unify the backend return format

But this can only handle global exceptions of the Controller layer. The filter is loaded earlier than the Controller, so it cannot be captured. To solve this problem, I wrote an ExceptionController to accept exceptions in the filter and then throw them. This can be captured and then wrapped and returned to the front end.

@restController public Class ExceptionController {/** * rethrow exception */ @requestMapping ("/filterException") public void FilterException (it request) throws the Exception {/ / return is Java. Lang. RuntimeException: Throw new RuntimeException(request.getAttribute("errMsg").toString().split(":")[1]); }}Copy the code

ExecuteLogin (Request, Response) methods are called if the token exists, in which Shiro will authenticate the token as a logged in user, and our own realm will be called to authenticate the user. And finally, in the filter, we’re going to rewrite the authentication failure method, and again, we’re going to send the exception to the controller and throw it.

@SneakyThrows @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest Request, ServletResponse Response) {RuntimeException RuntimeException = new RuntimeException(" Login status is invalid, Please log in again "); // Catch an exception and send it to expiredJwtException Request.setAttribute ("errMsg", runtimeException); / / will be distributed exception to request/filterException controller. The getRequestDispatcher ("/filterException "). The forward (request, response); return false; }Copy the code

Because the original operation of this method is just to set the failure message and return, not suitable for the mode of separation of the front and back ends, so I rewrite it to throw an exception so that the program can run normally, otherwise the front end will not receive the return message of the back end if the authentication fails.

The general process is as follows, so far the operation of separating the front and back ends is completed.

The key point of the authentication process is whether the token carried in the request is the same as accessToken in SimpleAuthenticationInfo Shiro authenticates the user by comparing the token information passed in by subjective.login (token) with the value of the second parameter in SimpleAuthenticationInfo, In this project, I use tokenId to mark the user (I call the token generated during login a tokenId, which is a value, and the token passed in the login method an object, which needs to be distinguished), so when creating the token object, I just need to pass the tokenId in. A common method, such as UsernamePasswordToken, is to pass in the username and password to generate a token object and then call the subject.login(token) method. The second parameter to SimpleAuthenticationInfo should be the password. Authentication compares the password to the password sent from the token. The first call to the subjection.login (token) method is authenticated by a realm and then new a SimpleAuthenticationInfo returns stored in Shiro’s cache, Then, the next authentication will determine whether the parameters in the token match the Info in the cache. This is why I said that you do not need to call the subject.login(token) at login time. SimpleAuthenticationInfo = SimpleAuthenticationInfo = SimpleAuthenticationInfo = SimpleAuthenticationInfo = SimpleAuthenticationInfo = SimpleAuthenticationInfo = SimpleAuthenticationInfo The difference is that the first time you go to the realm to get Info information, the rest of the information is directly to the cache, so the login to get token to ensure that the first time through the realm authentication. The above is just my personal understanding if there is any wrong place welcome to correct.