Original website:
Other web sites
Shiro instance series
Shiro -- instance -- springboot_ CSDN blog
Shiro -- instance -- springboot -- Shiro redis_ CSDN blog
Shiro -- instance -- springboot -- JWT_ CSDN blog (this article)
Reference website
shiro+jwt+springboot integration_ q89757316 blog - CSDN blog
Super detailed! 4 hours to develop a SpringBoot+vue front and rear end separation blog project!!
SpringBoot integrates JWT and Apache Shiro - Zhihu
brief introduction
explain
- This article practices the use of Shiro. Try to use the native Shiro configuration and minimize the custom configuration.
- Use jwt instead of the default authc as the authentication method, others remain unchanged.
- I passed my self-test and the code is available.
- This Shiro example is a series, and this article is one of them. The following articles are in this series
Use form
- Use jwt instead of the default session to manage permissions.
- Customize the jwt filter and register it with the Spring container under the name "authc"
- Use roles and resource permissions. (annotation)
- Use the Knife4j test (an upgraded version of Swagger).
- Use Shiro spring boot web starter: 1.7.0
Technology stack
- shiro-spring-boot-web-starter: 1.7.0
- spring-boot-starter-parent: 2.3.8.RELEASE
- mysql: 8.0
- Mybatis plus boot starter: 3.4.3.1 (persistence layer framework)
- lombok (simplified code)
- Knife4j spring boot starter: 3.0.3 (interface document (upgraded version of swagger))
Business scenario
This paper assumes a mall system.
The roles are as follows
- admin: have all permissions
- Use shiro wildcard: *:*
- productManager: has all permissions for product
- product:add,product:delete,product:edit,product:view
- We assign these four permissions to the role of productManager separately. In the actual project, we can assign the permission of product: * to the role of productManager.
- orderManager: has all the permissions of order
- order:add,product:delete,product:edit,product:view
- We assign these four permissions to the role of productManager separately. In the actual project, we can assign the permission of product: * to the role of productManager.
The user password and authority are as follows
user | password | role | Permissions owned |
zhang3 | 12345 | admin | All permissions. |
li4 | abcde | productManager | All permissions for the product. |
In order to test the function, the Department grants the following special permissions:
- Verify that shiro can control permissions through roles alone
- Set role permissions for the edit order interface:
- @RequiresRoles(value = {"admin,productManager"}, logical = Logical.OR)
- Set role permissions for the edit order interface:
- Verify that shiro cannot add @ RequiresRoles and @ RequiresPermissions to an interface at the same time
- Set role permissions and resource permissions for the interface viewing orders:
- @RequiresPermissions("order:view")
@RequiresRoles(value = {"admin,productManager"}, logical = Logical.OR)
- @RequiresPermissions("order:view")
- Set role permissions and resource permissions for the interface viewing orders:
code
Project structure
Table structure and data
In order to simplify the code, users, roles and permissions are directly generated to the database with SQL. In actual development, they need to be added, deleted and modified through the Controller.
business.sql
DROP DATABASE IF EXISTS shiro; CREATE DATABASE shiro DEFAULT CHARACTER SET utf8; USE shiro; DROP TABLE IF EXISTS t_user; DROP TABLE IF EXISTS t_role; DROP TABLE IF EXISTS t_permission; DROP TABLE IF EXISTS t_user_role_mid; DROP TABLE IF EXISTS t_role_permission_mid; create table t_user ( id bigint AUTO_INCREMENT, user_name VARCHAR(100), password VARCHAR(100), salt VARCHAR(100), PRIMARY KEY(id) ) charset=utf8 ENGINE=InnoDB; create table t_role ( id bigint AUTO_INCREMENT, name VARCHAR(100), description VARCHAR(100), PRIMARY KEY(id) ) charset=utf8 ENGINE=InnoDB; create table t_permission ( id bigint AUTO_INCREMENT, name VARCHAR(100), description VARCHAR(100), PRIMARY KEY(id) ) charset=utf8 ENGINE=InnoDB; create table t_user_role_mid ( id bigint AUTO_INCREMENT, user_id bigint, role_id bigint, PRIMARY KEY(id) ) charset=utf8 ENGINE=InnoDB; create table t_role_permission_mid ( id bigint AUTO_INCREMENT, role_id bigint, permission_id bigint, PRIMARY KEY(id) ) charset=utf8 ENGINE=InnoDB; -- Password: 12345 INSERT INTO `t_user` VALUES (1,''zhang3'',''a7d59dfc5332749cb801f86a24f5f590'',''e5ykFiNwShfCXvBRPr3wXg==''); -- password: abcde INSERT INTO `t_user` VALUES (2,''li4'',''43e28304197b9216e45ab1ce8dac831b'',''jPz19y7arvYIGhuUjsb6sQ==''); INSERT INTO `t_role` VALUES (1,''admin'',''Super administrator''); INSERT INTO `t_role` VALUES (2,''productManager'',''Product administrator''); INSERT INTO `t_role` VALUES (3,''orderManager'',''Order administrator''); INSERT INTO `t_permission` VALUES (1,''*:*'',''All permissions''); INSERT INTO `t_permission` VALUES (2,''product:add'',''Add products''); INSERT INTO `t_permission` VALUES (3,''product:delete'',''Delete product''); INSERT INTO `t_permission` VALUES (4,''product:edit'',''Edit product''); INSERT INTO `t_permission` VALUES (5,''product:view'',''View products''); INSERT INTO `t_permission` VALUES (6,''order:add'',''Add order''); INSERT INTO `t_permission` VALUES (7,''order:delete'',''Delete order''); INSERT INTO `t_permission` VALUES (8,''order:edit'',''Edit order''); INSERT INTO `t_permission` VALUES (9,''order:view'',''View order''); INSERT INTO `t_user_role_mid` VALUES (1,2,2); INSERT INTO `t_user_role_mid` VALUES (2,1,1); INSERT INTO `t_role_permission_mid` VALUES (1,1,1); INSERT INTO `t_role_permission_mid` VALUES (2,2,2); INSERT INTO `t_role_permission_mid` VALUES (3,2,3); INSERT INTO `t_role_permission_mid` VALUES (4,2,4); INSERT INTO `t_role_permission_mid` VALUES (5,2,5); INSERT INTO `t_role_permission_mid` VALUES (6,3,6); INSERT INTO `t_role_permission_mid` VALUES (7,3,7); INSERT INTO `t_role_permission_mid` VALUES (8,3,8); INSERT INTO `t_role_permission_mid` VALUES (9,3,9);
Configuration files and dependencies
application.yml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/shiro?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 222333 # Mybatis plus print SQL log mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl shiro: enabled: true # Enable shiro. The default value is true web: enabled: true # Enable shiro Web. The default value is true # loginUrl: /login # The login address is "login.jsp" by default # successUrl: /index # The address to jump after successful login. The default is "/" # unauthorizedUrl: /unauthorized # Unauthorized default jump address # sessionManager: # sessionIdCookieEnabled: true # Whether to allow session tracking through cookies. The default value is true. # sessionIdUrlRewritingEnabled: false # Whether to put JSESSIONID into the url. The default is true. # annotations: # enabled: true # Open shiro's annotation. For example: @ RequiresRole. # There are three ways to open shiro annotation. # 1. Introduce spring aop dependency: org.springframework.boot: spring boot starter AOP # 2. Set shiro.annotations.enabled=true in application.yml. # Correspondence: ShiroAnnotationProcessorAutoConfiguration.class # 3. Provide an AuthorizationAttributeSourceAdvisor bean # It is strongly recommended to use the first one, because only one of shiro's own AOPs and spring AOPs can be used. If shiro's AOPs are used, there will be many # The spring annotation is invalid. For example, @ Async, @ Cacheable # Custom configuration custom: jwt: secret: 7h4alejfloriaj5&asf!a4m # Key. Write whatever you want expire: 1800 # Valid time of token, 30 minutes. Unit: seconds
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.8.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>shiro_3_jwt</artifactId> <version>0.0.1-SNAPSHOT</version> <name>shiro_3_jwt</name> <description>shiro_3_jwt</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.7.1</version> </dependency> <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
Configuration (config package)
Code execution process
login
JwtFilter#onAccessDenied // Return true normally
PathMatchingFilter // Go to the anon filter and let it go if it's inside
Own login interface
Interfaces requiring permissions
JwtFilter#onAccessDenied // Execute executeLogin(servletRequest, servletResponse);
JwtFilter#createToken
AccountRealm#doGetAuthenticationInfo
AccountRealm#doGetAuthorizationInfo
Own interface
shiro general configuration
package com.example.demo.config.shiro; import com.example.demo.common.constant.WhiteList; import com.example.demo.config.shiro.filter.JwtFilter; import com.example.demo.config.shiro.realm.AccountRealm; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; @Configuration public class ShiroConfig { @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); chainDefinition.addPathDefinition("/login", "anon"); WhiteList.ALL.forEach(str -> { chainDefinition.addPathDefinition(str, "anon"); }); // all other paths require a logged in user chainDefinition.addPathDefinition("/**", "authc"); return chainDefinition; } // The name must be authc to replace shiro's default authc. @Bean("authc") public AuthenticatingFilter authenticatingFilter() { return new JwtFilter(); } @Bean public DefaultWebSecurityManager securityManager() { DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); // Close shiro's own session. In this way, shiro cannot be logged in through session. Later, you will log in with jwt credentials. // See: http://shiro.apache.org/session-management.html#SessionManagement-DisablingSubjectStateSessionStorage defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(getDatabaseRealm()); securityManager.setSubjectDAO(subjectDAO); return securityManager; } @Bean public AccountRealm getDatabaseRealm() { return new AccountRealm(); } /** * setUsePrefix(true)It is used to solve a strange bug. It is as follows: * When spring aop is introduced, add @ RequiresRole and so on to the methods of the class annotated by @ Controller * shiro Annotation will cause the method to be unable to map the request and return 404. Adding this configuration can solve this bug. */ @Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setUsePrefix(true); return defaultAdvisorAutoProxyCreator; } /** * Open shiro annotation. For example: @ RequiresRole * This method is not used here to open the annotation, but the spring aop dependency is introduced. See the annotation in application.yml for the reason */ /*@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }*/ /** * This configuration method will not work in this project. */ /* @Bean("shiroFilterFactoryBean") public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // Authentication failed. The address to jump to. // shiroFilterFactoryBean.setLoginUrl("/login"); // // Link to jump after successful login // shiroFilterFactoryBean.setSuccessUrl("/index"); // // Unauthorized interface; // shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); WhiteList.ALL.forEach(str -> { filterChainDefinitionMap.put(str, "anon"); }); // filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/**", "jwtAuthc"); Map<String, Filter> customisedFilters = new LinkedHashMap<>(); // Injection cannot be used to set the filter. If injection is used, this filter will have the highest priority (/ * * the highest priority, resulting in the invalidation of all previous requests). // springboot All classes that implement the javax.servlet.Filter interface will be scanned without adding @ Component. customisedFilters.put("jwtAuthc", new JwtFilter()); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); shiroFilterFactoryBean.setFilters(customisedFilters); return shiroFilterFactoryBean; }*/ }
Custom Realm
package com.example.demo.config.shiro.realm; import com.example.demo.rbac.permission.service.PermissionService; import com.example.demo.rbac.role.service.RoleService; import com.example.demo.rbac.user.entity.User; import com.example.demo.rbac.user.service.UserService; import com.example.demo.common.util.auth.JwtUtil; import com.example.demo.config.shiro.entity.JwtToken; import com.example.demo.common.entity.AccountProfile; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import java.util.Set; public class AccountRealm extends AuthorizingRealm { @Lazy @Autowired private UserService userService; @Lazy @Autowired private RoleService roleService; @Lazy @Autowired private PermissionService permissionService; //Authentication scheme to enable realm to support jwt @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } // Login authentication // SimpleAuthenticationInfo here can return any value, which will not be used in password verification. @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { JwtToken jwtToken = (JwtToken) token; String userId = JwtUtil.getUserIdByToken((String) jwtToken.getPrincipal()); if (userId == null) { throw new UnknownAccountException("token Null, please login again"); } // Get password in database User user = userService.getById(userId); if (user == null) { throw new UnknownAccountException("token Null, please login again"); } AccountProfile accountProfile = new AccountProfile(); accountProfile.setId(userId); accountProfile.setUserName(user.getUserName()); String salt = user.getSalt(); // The account and password are stored in the authentication information. getName() is the inheritance method of the current real, and usually returns the current class name: accountreal // The salt is also put in and automatically verified by the HashedCredentialsMatcher configured in ShiroConfig SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( accountProfile, jwtToken.getCredentials(), ByteSource.Util.bytes(salt), getName()); return authenticationInfo; } // Permission verification // Only the default filter in the org.apache.shiro.web.filter.authz package can be used here. @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // If you can enter here, it means that the account has passed the authentication AccountProfile profile = (AccountProfile) principalCollection.getPrimaryPrincipal(); // Obtain roles and permissions through service Set<String> permissions = permissionService.getPermissionsByUserId(profile.getId()); Set<String> roles = roleService.getRolesByUserId(profile.getId()); // Authorized object SimpleAuthorizationInfo s = new SimpleAuthorizationInfo(); // Put in the roles and permissions obtained through the service s.setStringPermissions(permissions); s.setRoles(roles); return s; } }
Custom jwt filter
package com.example.demo.config.shiro.filter; import com.example.demo.common.util.auth.JwtUtil; import com.example.demo.config.shiro.entity.JwtToken; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; public class JwtFilter extends AuthenticatingFilter { /** * All requests will come here (anon or not). * Return true: indicates that you are allowed to go down. The PathMatchingFilter will be followed to see if the path corresponds to anon, etc * Return false: indicates that you are not allowed to go down. */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; String token = request.getHeader(HttpHeaders.COOKIE); // Custom headers are also OK, but the browser will not save custom headers, which need to be saved by the front end itself // String token = request.getHeader("Authentication"); if (!StringUtils.hasText(token)) { return true; } else { boolean verified = JwtUtil.verifyToken(token); if (!verified) { return true; } } // This login does not call the login interface, but the shiro level login. // Inside, the createToken method below will be called return executeLogin(servletRequest, servletResponse); } /** * The token here will be passed to the doGetAuthenticationInfo method of the AuthorizingRealm subclass (AccountRealm here) as a parameter */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) { HttpServletRequest request = (HttpServletRequest) servletRequest; String token = request.getHeader(HttpHeaders.COOKIE); // Custom headers are also OK, but the browser will not save custom headers, which need to be saved by the front end itself // String token = request.getHeader("Authentication"); if (!StringUtils.hasText(token)) { return null; } return new JwtToken(token); } }
Custom jwt token
package com.example.demo.config.shiro.entity; import org.apache.shiro.authc.AuthenticationToken; /** * JwtToken Instead of the official UsernamePasswordToken, it is the carrier of Shiro's user name, password and other information, * The front and back ends are separated, and the server does not save the user state, so functions such as RememberMe are not required. */ public class JwtToken implements AuthenticationToken { private final String token; public JwtToken(String jwt) { this.token = jwt; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
Other configurations
Get the configuration in jwt the configuration file
Configuration class
package com.example.demo.config; import com.example.demo.config.properties.JwtProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JwtConfig { @Bean @ConfigurationProperties(prefix = "custom.jwt") public JwtProperties jwtProperties() { return new JwtProperties(); } }
Attribute class
package com.example.demo.config.properties; import lombok.Data; @Data public class JwtProperties { private String secret; private long expire; }
Knife4j configuration (interface document)
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class Knife4jConfig { @Bean(value = "defaultApi2") public Docket defaultApi2() { Docket docket=new Docket(DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder() .title("My title") .description("My description") // .termsOfServiceUrl("http://www.xx.com/") .contact(new Contact("daoren", "https://knife.blog.csdn.net", "xx@qq.com")) .version("1.0") .build()) //Group name .groupName("all") .select() //Specify the controller scan path. It can not be specific to the controller. It will scan all the data in the specified path .apis(RequestHandlerSelectors.basePackage("com.example.demo")) .paths(PathSelectors.any()) .build(); return docket; } }
Permissions (rbac package)
user
service
package com.example.demo.rbac.user.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.rbac.user.entity.User; public interface UserService extends IService<User> { User getUserByUserName(String userName); }
package com.example.demo.rbac.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.rbac.user.entity.User; import com.example.demo.rbac.user.mapper.UserMapper; import com.example.demo.rbac.user.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public User getUserByUserName(String userName) { return lambdaQuery().eq(User::getUserName, userName).one(); } }
mapper
package com.example.demo.rbac.user.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.rbac.user.entity.User; import org.springframework.stereotype.Repository; @Repository public interface UserMapper extends BaseMapper<User> { }
entity
package com.example.demo.rbac.user.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = false) @TableName("t_user") public class User { @TableId(value = "id", type = IdType.AUTO) private Long id; private String userName; private String password; private String salt; }
role
service
package com.example.demo.rbac.role.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.rbac.role.entity.Role; import java.util.Set; public interface RoleService extends IService<Role> { Set<String> getRolesByUserId(String userId); }
package com.example.demo.rbac.role.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.rbac.role.entity.Role; import com.example.demo.rbac.role.mapper.RoleMapper; import com.example.demo.rbac.role.service.RoleService; import org.springframework.stereotype.Service; import java.util.Set; @Service public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService { @Override public Set<String> getRolesByUserId(String userId) { Long id = Long.parseLong(userId); return this.getBaseMapper().getRolesByUserId(id); } }
mapper
package com.example.demo.rbac.role.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.rbac.role.entity.Role; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; import java.util.Set; @Repository public interface RoleMapper extends BaseMapper<Role> { @Select("SELECT " + " t_role.`name` " + "FROM " + " t_user, " + " t_user_role_mid, " + " t_role " + "WHERE " + " t_user.`id` = #{userId} " + " AND t_user.id = t_user_role_mid.user_id " + " AND t_user_role_mid.role_id = t_role.id") Set<String> getRolesByUserId(@Param("userId")Long userId); }
entity
package com.example.demo.rbac.role.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = false) @TableName("t_role") public class Role { @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; private String description; }
permission
service
package com.example.demo.rbac.permission.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.rbac.permission.entity.Permission; import java.util.Set; public interface PermissionService extends IService<Permission> { Set<String> getPermissionsByUserId(String userName); }
package com.example.demo.rbac.permission.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.rbac.permission.entity.Permission; import com.example.demo.rbac.permission.mapper.PermissionMapper; import com.example.demo.rbac.permission.service.PermissionService; import org.springframework.stereotype.Service; import java.util.Set; @Service public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService { @Override public Set<String> getPermissionsByUserId(String userId) { Long id = Long.parseLong(userId); return this.getBaseMapper().getPermissionsByUserId(id); } }
mapper
package com.example.demo.rbac.permission.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.rbac.permission.entity.Permission; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; import java.util.Set; @Repository public interface PermissionMapper extends BaseMapper<Permission> { @Select("SELECT " + " t_permission.`name` " + "FROM " + " t_user, " + " t_user_role_mid, " + " t_role, " + " t_role_permission_mid, " + " t_permission " + "WHERE " + " t_user.`id` = #{userId} " + " AND t_user.id = t_user_role_mid.user_id " + " AND t_user_role_mid.role_id = t_role.id " + " AND t_role.id = t_role_permission_mid.role_id " + " AND t_role_permission_mid.permission_id = t_permission.id") Set<String> getPermissionsByUserId(@Param("userId") Long userId); }
entity
package com.example.demo.rbac.permission.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = false) @TableName("t_permission") public class Permission { @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; private String description; }
Business (business package)
login
controller
package com.example.demo.business.login.controller; import com.example.demo.business.login.entity.LoginRequest; import com.example.demo.business.login.entity.LoginVO; import com.example.demo.rbac.user.entity.User; import com.example.demo.rbac.user.service.UserService; import com.example.demo.common.constant.AuthConstant; import com.example.demo.common.exception.BusinessException; import com.example.demo.common.util.auth.JwtUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.shiro.crypto.hash.SimpleHash; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; @Api(tags = "Sign in") @RestController public class LoginController { @Autowired private UserService userService; @ApiOperation("Sign in") @PostMapping("login") public LoginVO login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { String userName = loginRequest.getUserName(); String password = loginRequest.getPassword(); User user = userService.getUserByUserName(userName); if (user == null) { throw new BusinessException("user does not exist"); } String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE, password, user.getSalt(), AuthConstant.HASH_ITERATIONS).toString(); if (!user.getPassword().equals(calculatedPassword)) { throw new BusinessException("Username or password incorrect "); } String token = JwtUtil.createToken(user.getId().toString()); response.setHeader(HttpHeaders.SET_COOKIE, token); // Custom headers are also OK, but the browser will not save custom headers, which need to be saved by the front end itself // response.setHeader("Authentication", token); return fillResult(user); } private LoginVO fillResult(User user) { LoginVO loginVO = new LoginVO(); loginVO.setUserId(user.getId()); loginVO.setUserName(user.getUserName()); return loginVO; } }
entity
package com.example.demo.business.login.entity; import lombok.Data; @Data public class LoginRequest { private String userName; private String password; }
package com.example.demo.business.login.entity; import lombok.Data; @Data public class LoginVO { private Long userId; private String userName; }
Login (logout)
package com.example.demo.business.logout; import com.example.demo.common.entity.Result; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.shiro.SecurityUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = "Logout") @RestController public class LogoutController { @ApiOperation("Logout") @PostMapping("logout") public Result<Object> logout() { SecurityUtils.getSubject().logout(); return new Result(); } }
product
controller
package com.example.demo.business.product; import com.example.demo.common.entity.Result; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = "product") @RestController @RequestMapping("product") public class ProductController { @RequiresPermissions("product:add") @ApiOperation(value="Add products") @PostMapping("add") public Result add() { return new Result<>().message("product:add success"); } @RequiresPermissions("product:delete") @ApiOperation(value="Delete product") @PostMapping("delete") public Result delete() { return new Result<>().message("product:delete success"); } @RequiresPermissions("product:edit") @ApiOperation(value="Edit product") @PostMapping("edit") public Result edit() { return new Result<>().message("product:edit success"); } @RequiresPermissions("product:view") @ApiOperation(value="View products") @GetMapping("view") public Result view() { return new Result<>().message("product:view success"); } }
order
package com.example.demo.business.order; import com.example.demo.common.entity.Result; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = "order") @RestController @RequestMapping("order") public class OrderController { @RequiresPermissions("order:add") @ApiOperation(value="Add order") @PostMapping("add") public Result add() { return new Result<>().message("order:add success"); } @RequiresRoles(value = {"admin", "orderManager"}, logical = Logical.OR) @ApiOperation(value="Delete order") @PostMapping("delete") public Result delete() { return new Result<>().message("order:delete success"); } // Administrator or order administrator has permission @RequiresRoles(value = {"admin", "productManager"}, logical = Logical.OR) @ApiOperation(value="Edit order") @PostMapping("edit") public Result edit() { return new Result<>().message("order:edit success"); } // At this time, the conditions of both annotations must be met before access is allowed @RequiresPermissions("order:view") @RequiresRoles(value = {"admin", "productManager"}, logical = Logical.OR) @ApiOperation(value="View order") @GetMapping("view") public Result view() { return new Result<>().message("order:view success"); } }
Common (common package)
Startup class
Of course, it's not in the common package, but it's also a public thing. Let's put it here.
package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.demo.**.mapper") public class ShiroApplication { public static void main(String[] args) { SpringApplication.run(ShiroApplication.class, args); } }
Global exception handling
package com.example.demo.common.advice; import com.example.demo.common.entity.Result; import com.example.demo.common.exception.BusinessException; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @Slf4j @Order(Ordered.LOWEST_PRECEDENCE - 1) @RestControllerAdvice public class GlobalExceptionAdvice { @ExceptionHandler(Exception.class) public Result<Object> handleException(Exception e) throws Exception { log.error(e.getMessage(), e); // If a custom exception has @ ResponseStatus annotation, it will continue to be thrown if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) { throw e; } // This should be written in the actual project to prevent users from seeing detailed exception information // return new Result().failure().message.message("operation failed"); return new Result<>().failure().message(e.getMessage()); } @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(UnauthenticatedException.class) public Result<Object> handleUnauthenticatedException(Exception e) { log.error(e.getMessage(), e); return new Result<>().failure().message(e.getMessage()); } @ResponseStatus(HttpStatus.FORBIDDEN) @ExceptionHandler(UnauthorizedException.class) public Result<Object> handleUnauthorizedException(Exception e) { log.error(e.getMessage(), e); return new Result<>().failure().message(e.getMessage()); } @ExceptionHandler(BusinessException.class) public Result<Object> handleBusinessException(Exception e) throws Exception { log.error(e.getMessage(), e); // If a custom exception has @ ResponseStatus annotation, it will continue to be thrown if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) { throw e; } // This should be written in the actual project to prevent users from seeing detailed exception information // Return new result < > (). Failure(). Message ("operation failed"); return new Result<>().failure().message(e.getMessage()); } }
Global response processing
package com.example.demo.common.advice; import com.example.demo.common.entity.Result; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.util.AntPathMatcher; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.util.Arrays; import java.util.List; @Slf4j @Order(Ordered.LOWEST_PRECEDENCE) @ControllerAdvice public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> { private List<String> KNIFE4J_URI = Arrays.asList( "/doc.html", "/swagger-resources", "/swagger-resources/configuration", "/v3/api-docs", "/v2/api-docs"); @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { // If the type returned by the interface itself is ResultWrapper, no operation is required and false is returned // return !returnType.getParameterType().equals(ResultWrapper.class); return true; } @Override @ResponseBody public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof String) { // If the return value is of String type, it needs to be wrapped as String type. Otherwise, an error will be reported try { ObjectMapper objectMapper = new ObjectMapper(); Result<Object> result = new Result<>().data(body); return objectMapper.writeValueAsString(result); } catch (JsonProcessingException e) { throw new RuntimeException("serialize String error"); } } else if (body instanceof Result) { return body; } else if (isKnife4jUrl(request.getURI().getPath())) { // If it is an interface document uri, skip it directly return body; } return new Result<>().data(body); } private boolean isKnife4jUrl(String uri) { AntPathMatcher pathMatcher = new AntPathMatcher(); for (String s : KNIFE4J_URI) { if (pathMatcher.match(s, uri)) { return true; } } return false; } }
constant
White list
package com.example.demo.common.constant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public interface WhiteList { List<String> KNIFE4J = Arrays.asList( "/doc.html", "/swagger-resources", "/swagger-resources/configuration", "/v3/api-docs", "/v2/api-docs", "/webjars/**"); List<String> ALL = new ArrayList<>(KNIFE4J); }
code of response data
package com.example.demo.common.constant; public enum ResultCode { SUCCESS(1000, "Access successful"), SYSTEM_FAILURE(1001, "System exception"), ; private final int code; private final String description; ResultCode(int code, String description) { this.code = code; this.description = description; } public int getCode() { return code; } public String getDescription() { return description; } }
Authorization related
package com.example.demo.common.constant; public interface AuthConstant { String ALGORITHM_TYPE = "md5"; int HASH_ITERATIONS = 2; }
Entity class
Encapsulate response results
package com.example.demo.common.entity; import com.example.demo.common.constant.ResultCode; import lombok.Data; @Data public class Result<T> { private boolean success = true; private int code = ResultCode.SUCCESS.getCode(); private String message; private T data; public Result() { } public Result(boolean success) { this.success = success; } public Result<T> success(boolean success) { Result<T> result = new Result<>(success); if (success) { result.code = ResultCode.SUCCESS.getCode(); } else { result.code = ResultCode.SYSTEM_FAILURE.getCode(); } return result; } public Result<T> success() { return success(true); } public Result<T> failure() { return success(false); } /** * @param code {@link ResultCode#getCode()} */ public Result<T> code(int code) { this.code = code; return this; } public Result<T> message(String message) { this.message = message; return this; } public Result<T> data(T data) { this.data = data; return this; } }
Account information
package com.example.demo.common.entity; import com.example.demo.common.util.auth.ShiroUtil; import lombok.Data; /** * Deposit account information. * After logging in, this object will be instantiated and placed in the subject. * Get method: {@ link shiruotil#getprofile()} */ @Data public class AccountProfile { private String id; private String userName; }
abnormal
Custom exception (business exception)
package com.example.demo.common.exception; public class BusinessException extends RuntimeException{ public BusinessException() { super(); } public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); } }
Tool class
The holder of the ApplicationContext of Spring
package com.example.demo.common.util; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class ApplicationContextHolder implements ApplicationContextAware { private static ApplicationContext context; public void setApplicationContext(ApplicationContext context) throws BeansException { ApplicationContextHolder.context = context; } public static ApplicationContext getContext() { return context; } }
jwt tool class
package com.example.demo.common.util.auth; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import com.example.demo.common.util.ApplicationContextHolder; import com.example.demo.config.properties.JwtProperties; import java.util.Date; public class JwtUtil { private static JwtProperties jwtProperties; // Create jwt token public static String createToken(String userId) { if (jwtProperties == null) { jwtProperties = ApplicationContextHolder.getContext() .getBean(JwtProperties.class); } try { Date date = new Date(System.currentTimeMillis() + jwtProperties.getExpire() * 1000); Algorithm algorithm = Algorithm.HMAC512(jwtProperties.getSecret()); return JWT.create() // Customize the key value of the private payload. For example:. withClaim("userName", "Tony") // .withClaim("key1", "value1") .withAudience(userId) // Save the user id in the token .withExpiresAt(date) // After date, the token expires .sign(algorithm); // Key of token } catch (Exception e) { return null; } } // Verification token public static boolean verifyToken(String token) { if (jwtProperties == null) { jwtProperties = ApplicationContextHolder.getContext() .getBean(JwtProperties.class); } try { Algorithm algorithm = Algorithm.HMAC512(jwtProperties.getSecret()); JWTVerifier verifier = JWT.require(algorithm) // .withIssuer("auth0") // .withClaim("username", username) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (JWTVerificationException exception) { // Token errors, token expiration, etc. will come here return false; } } public static String getUserIdByToken(String token) { try { String userId = JWT.decode(token).getAudience().get(0); return userId; } catch (JWTDecodeException e) { return null; } } }
Shiro tool class
package com.example.demo.common.util.auth; import com.example.demo.common.entity.AccountProfile; import org.apache.shiro.SecurityUtils; public class ShiroUtil { // Used to obtain information about the current account. public static AccountProfile getProfile() { return (AccountProfile) SecurityUtils.getSubject().getPrincipal(); } }
test
Test super administrator (admin)
Start the project and access: http://localhost:8080/doc.html
1. Test login
- Login succeeded
- As you can see, a set cookie header will be returned, and the value is token.
2. Test interfaces with resource permissions
Product interfaces are added to the test.
- Successfully accessed.
- Cookie s are passed on request
I use standard: set Cookie, Cookie for authentication. If it is a custom header, you need to write it manually:
3. Test logout
4. Access the interface again
- Access succeeded.
- Because the token has not expired and the browser will send it to the server, it is successful.
Test product manager
Start the project and access: http://localhost:8080/doc.html
1. Test login
- Login succeeded
- As you can see, a set cookie header will be returned, and the value is token.
2. Test interfaces with resource permissions
Product interfaces are added to the test.
- Successfully accessed.
- Cookie s are passed on request
3. Test the interface without resource permission
We will test and add order interface.
- Access failed.
- Cookie s are passed on request
- One detail: the prompt is red. This is the role of @ ResponseStatus
Click in to see that the status code is specified by me: 403
4. Test the interface with role permission
We test and edit the order interface.
- Access succeeded.
- Cookie s are passed on request
4. Test the interface without role permission
We test the delete order interface.
- Access failed.
- Cookie s are passed on request
The test has both role and permission annotations
1. Log in (productManager)
2. Request an interface with both role and resource permission annotations
Request order viewing interface here.
- The request failed.
- Prompt no resource permission.
It can be inferred that if there are two annotations, they must be satisfied at the same time. Next, try using the admin role, which has all resource permissions at the same time.
3. Login (admin)
4. Request an interface with both role and resource permission annotations
Request order viewing interface here.
- Request succeeded
Note if there are two annotations, they must be satisfied at the same time.
Restart service re request
1. Login
Login succeeded
2. Restart the server
Restart the application started by Idea.
3. Access authorized interfaces
We have added interfaces for accessing products.
- As you can see, the access was successful.
Request after timeout
1. Modify the configuration file and temporarily shorten the token expiration time (10 seconds here)
application.yml
2. Login
3. Wait for more than 10 seconds before requesting
The request failed.
In my code, I specify this error as 401. Click to verify: