Preface
In today's web development, the authentication of security rights has always played an important role, so Spring has also produced its own security module, but this is a relatively heavy framework, configuration is quite cumbersome. Later, shiro, a lightweight security framework, emerged. The methods provided in it also basically meet the needs of developers.
With the emergence of springboot, the official provides a series of out of the box starter s, and security gradually returns to people's vision, forming the commonly used technology stacks such as springboot+security or ssm+shiro.
SQL script
The database here uses MySQL 5.7
/* SQLyog Ultimate v12.4.3 (64 bit) MySQL - 5.7.17-log : Database - security ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`security` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `security`; /*Table structure for table `menu` */ DROP TABLE IF EXISTS `menu`; CREATE TABLE `menu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `pattern` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; /*Data for the table `menu` */ insert into `menu`(`id`,`pattern`) values (1,'/db/**'), (2,'/admin/**'), (3,'/user/**'); /*Table structure for table `menu_role` */ DROP TABLE IF EXISTS `menu_role`; CREATE TABLE `menu_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `mid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; /*Data for the table `menu_role` */ insert into `menu_role`(`id`,`mid`,`rid`) values (1,1,1), (2,2,2), (3,3,3); /*Table structure for table `role` */ DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `nameZh` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; /*Data for the table `role` */ insert into `role`(`id`,`name`,`nameZh`) values (1,'ROLE_dba','Database Administrator'), (2,'ROLE_admin','system administrator'), (3,'ROLE_user','user'); /*Table structure for table `user` */ DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `enabled` tinyint(1) DEFAULT NULL, `locked` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; /*Data for the table `user` */ insert into `user`(`id`,`username`,`password`,`enabled`,`locked`) values (1,'root','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0), (2,'admin','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0), (3,'sang','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0); /*Table structure for table `user_role` */ DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; /*Data for the table `user_role` */ insert into `user_role`(`id`,`uid`,`rid`) values (1,1,1), (2,1,2), (3,2,2), (4,3,3); /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
Add dependency
<?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.2.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>leo.study</groupId> <artifactId>security</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.17</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Connect to database
spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql://localhost:3306/security spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
Create entity class first
package leo.study.security.bean; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ public class User implements UserDetails { private Integer id; private String username; private String password; private Boolean enabled; private boolean locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities=new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public void setLocked(boolean locked) { this.locked = locked; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } }
The user information table stores the user login name, password, account lock, account available flag and other information, so the user details is implemented to save our user information.
The first thing we should pay attention to is:
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; }
Here is the user's role information, and we have defined a role and role class, so to get the user's corresponding role, we need to put the role into the user object.
How to get the user's role here?
First, create a List, because the return type of the inherited method is Collection, and then the generic type of the Collection is a inheriting class of GrantedAuthority, simplegratedauthority, and then start to traverse the roles, and put the role name by instantiating simplegratedauthority
public SimpleGrantedAuthority(String role) { Assert.hasText(role, "A granted authority textual representation is required"); this.role = role; }
Second, we should pay attention to:
I have enabled and locked fields in the database here, and the implemented UserDetails also returns these two fields to us. This is whether the account has not been locked. Pay attention to Non, so you need to reverse and tell him, yes, it has not been locked! I don't think there's anything else to say.
@Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isEnabled() { return enabled; }
package leo.study.security.bean; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ public class Role { private Integer id; private String name; private String nameZh; @Override public String toString() { return "Role{" + "id=" + id + ", name='" + name + '\'' + ", nameZh='" + nameZh + '\'' + '}'; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
Menu
The menu class should put the roles in, and then look at their corresponding menus according to the user roles
package leo.study.security.bean; import java.util.List; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ public class Menu { private Integer id; private String pattern; private List<Role> roles; @Override public String toString() { return "Menu{" + "id=" + id + ", pattern='" + pattern + '\'' + ", roles=" + roles + '}'; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } }
Prepare UserService
UserService needs to implement UserDetailsService
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return null; }
As the name implies, loadUserByUsername queries the user information through the user name. First, it determines whether the user exists, if so, it determines what role he has, and if not, it throws an exception prompt.
package leo.study.security.service; import leo.study.security.bean.User; import leo.study.security.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ @Service public class UserService implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user=userMapper.loadUserByUsername(username); //Judge if the query result is empty if(user==null){ throw new UsernameNotFoundException("user does not exist"); } user.setRoles(userMapper.getRolesById(user.getId())); return user; } }
Corresponding mapper class
package leo.study.security.mapper; import leo.study.security.bean.Role; import leo.study.security.bean.User; import java.util.List; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ public interface UserMapper { User loadUserByUsername(String username); List<Role> getRolesById(Integer id); }
mapper's profile
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="leo.study.security.mapper.UserMapper"> <select id="loadUserByUsername" resultType="leo.study.security.bean.User"> select * from user where username=#{username}; </select> <select id="getRolesById" resultType="leo.study.security.bean.Role"> select * from role where id in(select rid from user_role where uid=#{id}); </select> </mapper>
Start writing configuration class
package leo.study.security.config; import leo.study.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; //encryption @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } }
Some of the bags on it have not been removed. I don't care. First of all, BCryptPasswordEncoder is password encryption. The password submitted by the current security must be encrypted. Second, the @ Configuration annotation that I made has forgotten to add. No wonder I can't find the problem for half a day.
Write a controller
@RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello"; }
Start testing
security will default to its login page
Request after login, no problem!
Of course, this is the most basic. What we want to achieve is to enable the corresponding users to access the path they should access.
Query the menu path that the corresponding user can access
We have prepared the entity class before, and now write the service layer
menuservice
package leo.study.security.service; import leo.study.security.bean.Menu; import leo.study.security.bean.Role; import leo.study.security.mapper.MenuMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ @Service public class MenuService { @Autowired MenuMapper menuMapper; public List<Menu> getAllMenus(){ return menuMapper.getAllMenus(); } }
MenuMapper
package leo.study.security.mapper; import leo.study.security.bean.Menu; import java.util.List; public interface MenuMapper { List<Menu> getAllMenus(); }
configuration file
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="leo.study.security.mapper.MenuMapper"> <resultMap id="BaseResultMap" type="leo.study.security.bean.Menu"> <id property="id" column="id"></id> <result property="pattern" column="pattern"></result> <collection property="roles" ofType="leo.study.security.bean.Role"> <id property="id" column="rid"></id> <result property="name" column="rname"></result> <result property="nameZh" column="rnameZh"></result> </collection> </resultMap> <select id="getAllMenus" resultMap="BaseResultMap"> select m.*,r.id rid,r.name rname,r.nameZh rnameZh from menu m left join menu_role mr on m.id=mr.mid left join role r on mr.rid=r.id </select> </mapper>
After preparation, write a filter in the configuration class to obtain the request address of the current user and compare it with all his permissions to see whether it is consistent. He only makes comparisons here. There will be a processing class later
package leo.study.security.config; import leo.study.security.bean.Menu; import leo.study.security.bean.Role; import leo.study.security.service.MenuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import java.util.Collection; import java.util.List; /** * @description: * Main functions: * Analysis request address * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ @Component public class MyFilter implements FilterInvocationSecurityMetadataSource { //Path matching AntPathMatcher antPathMatcher=new AntPathMatcher(); @Autowired MenuService menuService; @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { //Get the requested address String requestUrl = ((FilterInvocation) o).getRequestUrl(); List<Menu> list = menuService.getAllMenus();//Get the request path corresponding to all user roles for (Menu menu : list) { //Start to compare whether the path obtained by the database is consistent with the request path if(antPathMatcher.match(menu.getPattern(),requestUrl)){ //When the path is completely consistent, it depends on which roles the path needs to request List<Role> roles = menu.getRoles(); //Create an array and place roles String[] roleStr=new String[roles.size()]; //Consider that a person may have multiple roles and need to traverse for (int i = 0; i < roles.size(); i++) { //Get role name roleStr[i]=roles.get(i).getName(); } //Return to character object return SecurityConfig.createList(roleStr); } } //If the requested method is not judged by the above methods, a role login flag will be returned. When you request the above methods, you cannot //When identifying the path, automatically jump to login return SecurityConfig.createList("ROLE_login"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return false; } }
MyAccessDecisionManager
package leo.study.security.config; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ @Component public class MyAccessDecisionManager implements AccessDecisionManager { //authentication saves the login information of the current user //o get current request object //Return value of collection public collection < configattribute > getattributes (object o) @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { //Traversal collection for (ConfigAttribute attribute : collection) { if("ROLE_login".equals(attribute.getAttribute())){ if(authentication instanceof AnonymousAuthenticationToken){ throw new AccessDeniedException("Illegal request"); }else { return; } } //This is the role of the login user Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { if(authority.getAuthority().equals(attribute.getAttribute())){ return; } } } throw new AccessDeniedException("Illegal request"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
Add configuration class
package leo.study.security.config; import leo.study.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; /** * @description: * @author: Leo * @createDate: 2020/2/11 * @version: 1.0 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Autowired MyFilter myFilter; @Autowired MyAccessDecisionManager myAccessDecisionManager; //encryption @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(myFilter); o.setAccessDecisionManager(myAccessDecisionManager); return o; } }).and().formLogin().permitAll().and().csrf().disable(); } }