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:
- The user calls the login request, which is called at login time SecurityUtils.getSubject().login(token)
- shiro calls the doGetAuthenticationInfo method of the AuthorizingRealm class to verify
- 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:
- OncePerRequestFilter: make sure to call doFilterInternal only once for a request, that is, if the internal forward does not execute doFilterInternal again
- AdviceFilter: AdviceFilter provides AOP functions, and its implementation is the same as the concept of Interceptor in spring MVC
- 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
- AccessControlFilter (common): inherits the PathMatchingFilter and extends two methods, isAccessAllowed and onAccessDenied
- UserFilter: inherits the AccessControlFilter, indicating the Interceptor Based on whether the user is logged in. It can be used directly
- RolesAuthorizationFilter: inherits AccessControlFilter, which means interception filter based on special roles. It can be used directly
- 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:
- Custom filter
- Register filters in the interceptor factory, such as factoryBean.getFilters().put(“c_once”, new MyOncePerRequestFilter());
- 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:
- isAccessAllowed: whether access is allowed, return true to indicate that access is allowed
- 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:
- Using a filter, first verify whether you are logged in and then verify whether you have permission, configured as / api/**=c_login_and_perms
- 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); }); } }