Shiro and springboot integration

Keywords: Shiro Session Apache Spring

Shiro and springboot integration

summary

Apache Shiro is a powerful and easy-to-use Java security framework, which performs authentication, authorization, encryption and session management. Using shiro, you can easily complete the development of the project rights management module

Three core components:

  • Subject: the current operating user. It can be a login user or a third-party access program
  • Security Manager: manages security operations for all users, including authentication, authorization, encryption, and session management
  • Realm s: when authenticating and authorizing users, Shiro will find users and their permission information from the Realms configured by the application. In essence, it is a DAO that can query users' permission

Architecture diagram

[the external link image transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-o61b3lbe-1592722707159)( shiro.jpg ]

maven dependency

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>1.5.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.5.3</version>
    </dependency>
</dependencies>

Shiro configuration

In the springboot project, the relevant configurations are implemented by Java code plus configuration files. To use shiro, you must configure it as follows

@Configuration
public class ShiroConfig {

    /**
     * Configure SecurityManager
     */
    @Bean
    @Autowired
    public DefaultWebSecurityManager securityManager(
            AuthRealm authRealm, DefaultWebSessionManager sessionManager,
            AbstractCacheManager cacheManager) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //Set Realm
        manager.setRealm(authRealm);
        //Set up sessionManager
        manager.setSessionManager(sessionManager);
        //Set cache management
        manager.setCacheManager(cacheManager);
        return manager;
    }
    /**
     * To enable aop annotation support, you need to use it with the advisor autoproxycreator method
     */
    @Bean
    @Autowired
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * Turn on Shiro's comments (e.g@ RequiresRoles,@RequiresPermissions )Assuming the @ RequiresPermissions("123") annotation on the controller method, it and / xxx = C in the configuration file_ Perms [123] equivalent
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    /**
     * shiro The filter manager will explain in detail later
     */
    @Bean
    @Autowired
    @ConfigurationProperties(prefix = "using.shiro.filter")
    public ShiroFilterFactoryBean shiroFilterFactory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        // The custom filter here can't be left to Spring to manage important things three times
        factoryBean.getFilters().put("c_user", new UserFilter());
        factoryBean.getFilters().put("c_perms", new PermissionsAuthorizationFilter());
        return factoryBean;
    }

    /**
     * Set session manager such as timeout
     */
    @Bean
    @ConfigurationProperties(prefix = "using.shiro.session")
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager manager = new DefaultWebSessionManager();
        // All session s must set the id into the Cookie, and the operation template with Cookie must be provided -- >
        manager.setSessionIdCookie(this.sessionIdCookie());
        manager.setSessionListeners(Arrays.asList(shiroSessionListener));
        manager.setSessionDAO(sessionDAO);

        return manager;
    }
    /**
     * Set cache manager
     */
    @Bean
    public AbstractCacheManager cacheManager() {
        MemoryConstrainedCacheManager manager = new MemoryConstrainedCacheManager();
        return manager;
    }
    /**
     * Set cookie s
     */
    @Bean
    @ConfigurationProperties(prefix = "using.shiro.cookie.sessionId")
    public SimpleCookie sessionIdCookie() {
        SimpleCookie cookie = new SimpleCookie();
        return cookie;
    }

}

The above configuration class requires some specific configuration items, which are shown in the yml file below

using:
  shiro:
    filter: #It will be used when filling the filter
      loginUrl: /login.html #Login page
      successUrl: /index.html #Home page after login
      unauthorizedUrl: /index403.html #Pages not authorized
      # Must have '|' to keep line breaks  
      # anon indicates that no authentication is required for direct access. c_user indicates that C is needed_ Execute the filter corresponding to user, such as UserFilter
      # factoryBean.getFilters().put("c_user", new UserFilter(sessionManager, userService));
      filterChainDefinitions: |
        /static/**=anon 
        /login.html=anon
        /index403.html=anon
        /login=anon
        /logout=c_user
        /menu/queryMenu=c_user
    session:
      # Timeout 30 minutes
      globalSessionTimeout: 1800000
      deleteInvalidSessions: true
      # One minute scan
      sessionValidationInterval: 60000
      sessionValidationSchedulerEnabled: true
      # Define the operation enabling of sessionIdCookie template
      sessionIdCookieEnabled: true
    cookie:
      sessionId:
        # JSESSIONID by default
        name: token
        # Ensure that the system will not be supplied by cross domain script operation. The default value is false
        httpOnly: true
        # In seconds, the default value is - 1, which means the browser is closed and the Cookie disappears
        maxAge: -1
        

Shiro's certification

Authentication: it can be understood as login, that is, who can prove that he is himself in the application. If provide ID card, user name to prove. In shiro, users need to provide principles and credentials to shiro so that the application can verify the user's identity:

Principles: identity, that is, the identity attribute of the principal, such as user name, mailbox, etc., is unique. A principal can have multiple principals, but only one primary principal, usually user name / password / mobile number.

Credentials: certificates / credentials, that is, security values known only to the principal, such as passwords / digital certificates.

Code example

@RestController
public class LogController{
    @PostMapping("login")
    public String login(@RequestParam("username") String userName,
                        @RequestParam("password") String password){
          //1. Get Subject and create user name / password authentication Token 
          Subject subject = SecurityUtils.getSubject();  
          //The token type (UsernamePasswordToken) here should be followed by AuthorizingRealm.doGetAuthenticationInfo Method parameter types are consistent
          UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
          try {  
              //2. Login, i.e. authentication, and cache session and permission information 
              subject.login(token);  
          } catch (AuthenticationException e) {  
              //3. Authentication failed 
              return "Login failed"; 
          }
        return "Login successful";
    }
    
    @PostMapping("logout")
    public String logout() {
        //1. Get users 
     Subject subject = SecurityUtils.getSubject();
     //2. Exit and clear the session permission and other cache information
     subject.logout();

     return "Exit successful";
    }
}

Shiro's Realm

Realm get user's permission information

//Custom Realm inherits authoringrealm
@Component
public class AuthRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private DefaultWebSessionManager sessionManager;

    /***
     * Obtain the authentication information, which will be called automatically when logging in, and verify the user according to the user name and password
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
        //The token type here should be consistent with the token type at login time
        if (!(authToken instanceof UsernamePasswordToken)) {
            throw new AuthenticationException("Unknown authentication type, Currently only user name and password authentication is supported");
        }
        UsernamePasswordToken pwdToken = (UsernamePasswordToken) authToken;
        //Query user by user name
        User user = this.userService.getUserByUsername(pwdToken.getUsername());

        if (user == null) {
            throw new UnknownAccountException();
        }
        //Store the queried user name and password to the AuthenticationInfo class, and then verify whether the user login password is consistent with the queried password in the framework
        //If it is consistent, the login succeeds; otherwise, the login fails
        AuthenticationInfo info = new SimpleAuthenticationInfo(user.getUserName(), user.getPassword(),
                this.getClass().getName());

        Subject subject = SecurityUtils.getSubject();
        Session session = subject.getSession();
        user.setPassword(null);
        session.setAttribute("USER_INFO", user);

        return info;
    }
    /***
     * Get permission information will be called automatically when logging in, but the above method doGetAuthenticationInfo must be executed first
     * And cache the permission information
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //Get users
        Subject subject = SecurityUtils.getSubject();
        //Get session according to user
        Session session = subject.getSession();
        Object sysUserObj = session.getAttribute("USER_INFO");
        User userInfo = (User) sysUserObj;
        //Store role information
        Set<String> roleSet = new HashSet<>();
        //Save permission set
        Set<String> permissionSet = new HashSet<>();
        // Query user role query resource permission information of user role with user-defined userService
        List<Resource> resources = userService.getResourcesByRole(userInfo.getRoleNo());
        for (Resource resource : resources) {
            // Here it must be combined with the configuration file / xxx=c_perms["RES_ID_100 "] or annotation @ RequiresPermissions("RES_ID_100 ") consistent
            permissionSet.add("RES_ID_" + resource.getId());
        }
        //Create return data
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //Set roles
        authorizationInfo.setRoles(roleSet);
        //Set all resource permissions
        authorizationInfo.setStringPermissions(permissionSet);
        return authorizationInfo;
    }
}

Implementation process of certification:

  1. The user calls the login request, which is called at login time SecurityUtils.getSubject().login(token)
  2. shiro calls the doGetAuthenticationInfo method of the AuthorizingRealm class to verify
  3. After verification, call the doGetAuthorizationInfo method of the AuthorizingRealm class to query the permission information of the user

filter

Default filter in shiro

Filter name Filter class remarks
anon org.apache.shiro.web.filter.authc.AnonymousFilter Anonymous interceptor, that is, it can be accessed without login; it is generally used for static resource filtering; example "/ static/**=anon"
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter Form based interceptor, such as "/ * * = authc, will jump to the corresponding login page if there is no login
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP authentication interceptor, if it fails, jump to the login page
logout org.apache.shiro.web.filter.authc.LogoutFilter Exit interceptor, example "/ logout=logout"
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter Do not create a session interceptor, call subject.getSession(false) no problem, but if subject.getSession(true) a DisabledSessionException exception will be thrown;
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter Permission interceptor to verify whether the user has all the permissions specified in the URL; example "/ user/**=perms"
port org.apache.shiro.web.filter.authz.PortFilter Port interceptor, main property: port (80): port that can pass through; example "/ test= port[80]", if the user accesses the page is not 80, the request port will be automatically changed to 80 and redirected to the 80 port, and other paths / parameters are the same
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter The rest style interceptor automatically builds the permission string according to the request method (GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create); the example "/ users=rest[user]" will automatically spell out“ user:read,user:create,user:update,user:delete ”Permission string for permission matching (all must be matched, isPermittedAll);
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter Role authorization interceptor to verify whether the user has all roles specified in the URL;
ssl org.apache.shiro.web.filter.authz.SslFilter SSL interceptor can only pass through if the request protocol is https; otherwise, it will automatically jump to https port (443); other interceptors are the same as port interceptors;
user org.apache.shiro.web.filter.authc.UserFilter User interceptor, user authentication has / remembers all I log in; example "/ * * = user

Custom filter

Custom filters can inherit the following classes:

  1. OncePerRequestFilter: make sure to call doFilterInternal only once for a request, that is, if the internal forward does not execute doFilterInternal again
  2. AdviceFilter: AdviceFilter provides AOP functions, and its implementation is the same as the concept of Interceptor in spring MVC
  3. PathMatchingFilter: PathMatchingFilter inherits the AdviceFilter and provides the function of url mode filtering. If you need to process the specified request, you can extend the PathMatchingFilter
  4. AccessControlFilter (common): inherits the PathMatchingFilter and extends two methods, isAccessAllowed and onAccessDenied
  5. UserFilter: inherits the AccessControlFilter, indicating the Interceptor Based on whether the user is logged in. It can be used directly
  6. RolesAuthorizationFilter: inherits AccessControlFilter, which means interception filter based on special roles. It can be used directly
  7. Permissions authorization filter (commonly used): inherit the AccessControlFilter to verify whether a URL request has access rights. You can use it directly

For the custom filter to take effect, you must perform the following steps:

  1. Custom filter
  2. Register filters in the interceptor factory, such as factoryBean.getFilters().put(“c_once”, new MyOncePerRequestFilter());
  3. Set the filter chain of the URL, such as factoryBean.getFilterChainDefinitionMap().put("/once/**", “c_once”)

Generally, the above 23 steps are specified in the configuration class, that is, the user-defined filter cannot be managed by spring. Remember to remember

OncePerRequestFilter

Ensure that only one call of doFilterInternal is made for one request, i.e. the internal forward will not execute doFilterInternal again

public class MyOncePerRequestFilter extends OncePerRequestFilter {  
    @Override  
    protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {  
        System.out.println("=========once per request filter");  
        chain.doFilter(request, response);  
    }  
}   

AdviceFilter

The AdviceFilter provides AOP functions, and its implementation is the same as the interceptor idea in spring MVC

public class MyAdviceFilter extends AdviceFilter {  
    @Override  
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {  
        System.out.println("Preprocessing, returning false Execution of subsequent interceptors will be interrupted");  
        return true;  
    }  
    @Override  
    protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {  
        System.out.println("Execute after normal return after executing interceptor chain");  
    }  
    @Override  
    public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {  
        System.out.println("Post final processing, no matter normal or abnormal end, will enter this method");  
    }  
}   

PathMatchingFilter

The PathMatchingFilter inherits the AdviceFilter and provides the function of url mode filtering. If you need to process the specified request, you can extend the PathMatchingFilter

public class MyPathMatchingFilter extends PathMatchingFilter {  
    //When the preHandle method is called, the following methods are called back
    @Override  
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
       System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));  
       return true;  
    }  
}   

AccessControlFilter

Inherits the PathMatchingFilter and extends two methods:

  1. isAccessAllowed: whether access is allowed, return true to indicate that access is allowed
  2. onAccessDenied: whether to handle by yourself when access is denied. If true is returned, it means that you do not handle and continue to execute the interceptor chain. If false is returned, it means that you have handled it
public class MyAccessControlFilter extends AccessControlFilter {  
    //Whether access is allowed. Return true to indicate whether access is allowed
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
        System.out.println("access allowed");  
        return true;  
    }  
    //Whether to handle by yourself when access is denied. If you return true, it means you do not handle and continue to execute the interceptor chain. If you return false, it means you have handled it
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {  
        System.out.println("Access denial is not handled by itself. Continue the execution of interceptor chain");  
        return true;  
    }  
}  

UserFilter

Inherit the AccessControlFilter, which means the Interceptor Based on user login, that is, judge whether the user logs in or not. In general, this class can be used directly.

Requirement: the administrator can modify the user's status, such as activation and deactivation. Assuming that a user's status is activation and login to the system, the administrator updates the status to deactivation,
According to the UserFilter class provided by the jar package, as long as the user does not exit, the system can still be used. The current requirement is that as long as the administrator updates the status to disabled,
The login user needs to be forced to exit to access the system

public class MyUserFilter extends UserFilter {
 private static final Logger LOGGER = LoggerFactory.getLogger(UserFilter.class);
 private DefaultWebSessionManager sessionManager;
 private UserService userService;

    public UserFilter(DefaultWebSessionManager sessionManager, UserService userService) {
  this.sessionManager = sessionManager;
  this.userService = userService;
 }
    /**
    * Whether access is allowed. Return true to indicate whether access is allowed 
    * The meaning of the method of the parent class is: if the login user, access is allowed; otherwise, access is not allowed
    * Suppose the requirement is: the administrator can modify the user's status, such as opening and disabling,
    *           Suppose that the original opened user, the update status of the administrator is disabled, and the logged in user needs to be exited after the status update
    * 
    */
 @Override
 protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse,
                                      Object mappedValue) {
        boolean allowed = super.isAccessAllowed(servletRequest, servletResponse, mappedValue);
        if (allowed && !isLoginRequest(servletRequest, servletResponse)) {
            Subject subject = getSubject(servletRequest, servletResponse);
            Session session = subject.getSession();
            String username = subject.getPrincipal().toString();
            user user = this.userService.getUserByUsername(username);
            // Detect user status
            if (user == null || user.getState() != SysUserConstant.STATE_NORMAL) {
                subject.logout();
                this.sessionManager.getSessionIdCookie().setValue(Cookie.DELETED_COOKIE_VALUE);
                return false;
            }
        }
        return allowed;
    }

    /**
    * Whether to handle by yourself when access is denied. If you return true, it means you do not handle and continue to execute the interceptor chain. If you return false, it means you have handled it
    * The method of the parent class means: redirect to the login page without prompt
    * Suppose the requirement is: all requests are ajax requests, and return a custom class, which has relevant prompt information
    * 
    */
 @Override
 protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
  HttpServletRequest request = (HttpServletRequest) servletRequest;
  HttpServletResponse response = (HttpServletResponse) servletResponse;

  boolean isAjax = RequestUtils.isAjax(request);
  response.setStatus(HttpStatus.UNAUTHORIZED.value());

  if (isAjax) {
   JsonResult<String> jsonResult = new JsonResult<>();
   jsonResult.setErrorMessage(ErrorEnum.AUTH_NOT_LOGIN);
   response.getWriter().write(JSONObject.toJSONString(jsonResult));
   return false;
  } else {
   return super.onAccessDenied(servletRequest, servletResponse);
  }
 }
}

Filter chain

If there is only one filter for a URL, then this filter is a chain. If multiple filters are configured for a URL, then these filters form a filter chain. The processing order of the filters in the chain is the same as that in the configuration.

Requirement: if you need to verify whether the user logs in first, and then whether the user has this permission, there are two processing methods as follows:

  1. Using a filter, first verify whether you are logged in and then verify whether you have permission, configured as / api/**=c_login_and_perms
  2. Two filters are used, one is to verify whether to log in, and the other is to verify whether to have permission. The configuration is / api/**=c_login,c_perms, execute C first_ Filter corresponding to login, and then execute C_ Filter corresponding to perms

Regenerate filter chain

Why do we need to regenerate the filter chain?

We take UserFilter and PermissionsAuthorizationFilter provided by shiro framework as examples to illustrate.

Generally, our configuration files are as follows:

shiro:
  filter: #It will be used when filling the filter
    loginUrl: /login.html #Login page
    successUrl: /index.html #Home page after login
    unauthorizedUrl: /index403.html #Page 403 not authorized
    # Must have '|' to keep line breaks  
    # anon indicates that no authentication is required for direct access. 
    # c_user indicates that C is needed_ To execute the filter corresponding to user, such as the UserFilter provided by the framework, you need to configure it in the factorybean of the configuration class
    # factoryBean.getFilters().put("c_user", new UserFilter());
    # c_perms indicates that C is needed_ The filters corresponding to perms, such as the permissionsauuthorizationfilter provided by the framework, also need to be configured in the configuration class
    # factoryBean.getFilters().put("c_perms", new, such as PermissionsAuthorizationFilter());
    # /**=c_ Never use this in perms configuration. Say the important thing three times,
    # The reason is that all requests are in the isAccessAllowed method. Because the mappedValue parameter is null, it will directly return true, which can't achieve the effect of permission verification. See the following description for the detailed reasons
    filterChainDefinitions: |
      /static/**=anon 
      /login.html=anon
      /index403.html=anon
      /login=anon
      /logout=c_user
      /menu/queryMenu=c_user 
      /**=c_user,c_perms

First of all, in the above configuration file, / * * = c_user,c_perms, if you want to configure like this, there will be problems. Why?

In the method public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue), what is the mappedValue parameter and what is its use?

If configured as / hello=c_perms ["helloworld"], when initializing the permissionsauthorizationfilter class (userfilter and all subclasses inherited from PathMatchingFilter are the same),
In the parent class PathMatchingFilter, there is a map attribute appliedPaths. The key of the map is / hello in the configuration, and the value is helloworld.
The mappedValue parameter of the isAccessAllowed method is the value corresponding to the matching request URL in the appliedPaths. If the request matches / hello, the mappedValue value is helloworld.
If configured as / hello=c_perms, when the request matches to / hello, the value of mappedValue of isAccessAllowed method is null.

The isAccessAllowed method of the UserFilter class provided in the framework does not care about the mappedValue parameter, so it can be directly configured as / hello=c_user´╝î
However, in the isAccessAllowed method of the PermissionsAuthorizationFilter class provided by the framework, the mappedValue parameter is used, and it is used as permission for permission verification,
If the mappedValue parameter is empty, return true directly to indicate that the verification is passed. If the mappedValue parameter is not empty, get the value and go to shiro framework for permission verification,
Therefore, it cannot be simply configured as / hello=c_perms, which needs to be configured as / hello=c_perms[xxx], but there are many request URL s in a system. We can't configure all of them into the configuration file,
What shall I do?

That's why the filter chain has to be regenerated.

After the shiro framework obtains the shiro configuration, we need to read all URLs from the database and assemble them into / URL = C before the springboot system is started_ In the form of perms [XXX], write the filter chain again
The specific code is as follows:

@Component
public class ShiroFilterManger {

    @Autowired
    private ShiroFilterFactoryBean shiroFilterFactoryBean;
    @Autowired
    private UserService userService;

    @PostConstruct
    public synchronized void reloadFilterChainDefinition() throws Exception {
        // Get preset filter chain
        Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
        Map<String, String> newFilterChainDefinitionMap = new LinkedHashMap<>();
        newFilterChainDefinitionMap.putAll(filterChainDefinitionMap);
        // Query all resources in the database
        List<Resource> resources = userService.getAllResources();
        for (Resource resource : resources) {
            newFilterChainDefinitionMap.put(resource.getResourceUrl(),"c_user,c_perms[\"" + "RES_ID_"+resource.getId() + "\"]");
        }

        // Regenerate filter chain
        AbstractShiroFilter shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
        PathMatchingFilterChainResolver chainResolver = (PathMatchingFilterChainResolver) shiroFilter
                .getFilterChainResolver();
        DefaultFilterChainManager chainManager = (DefaultFilterChainManager) chainResolver.getFilterChainManager();
        chainManager.getFilterChains().clear();
        newFilterChainDefinitionMap.forEach((linkUrl, permission) -> {
            chainManager.createChain(linkUrl, permission);
        });
    }
}

Posted by imcomguy on Sun, 21 Jun 2020 03:35:26 -0700