大纲 前言 版本说明 组件 版本 SpringBoot 2.2.1.RELEASE Spring Security 5.2.1.RELEASE
用户授权介绍 用户授权是指验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
校验方式 权限校验 hasAuthority
:只能校验一个权限,例如 USER
,返回的 UserDetails
的 Authority
只要与这里匹配即可访问被保护的资源hasAnyAuthority
:可以校验多个权限,使用逗号分隔,例如 USER,ADMIN
,只要用户拥有其中任意一个权限即可访问被保护的资源角色校验 hasRole
:只能校验一个角色,例如 ROLE_USER
,在返回的 UserDetails
的 Authority
中需要添加 ROLE_
前缀,例如 ROLE_USER
hasAnyRole
:可以校验多个角色,使用逗号分隔,例如 ROLE_USER,ROLE_ADMIN
,只要用户拥有其中任意一个角色即可访问被保护的资源,在返回的 UserDetails
的 Authority
中需要添加 ROLE_
前缀,例如 ROLE_USER
校验方式的区别 hasAuthority
的源码如下,最终调用了 access
方法,传入了权限表达式 hasAuthority('xxx')
。
1 2 3 4 5 6 7 public ExpressionInterceptUrlRegistry hasAuthority (String authority) { return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority)); } private static String hasAuthority (String authority) { return "hasAuthority('" + authority + "')" ; }
hasRole
的源码如下,会自动给传入的字符串加上 ROLE_
前缀
1 2 3 4 5 6 7 8 9 10 11 12 13 public ExpressionInterceptUrlRegistry hasRole (String role) { return access(ExpressionUrlAuthorizationConfigurer.hasRole(role)); } private static String hasRole (String role) { Assert.notNull(role, "role cannot be null" ); if (role.startsWith("ROLE_" )) { throw new IllegalArgumentException( "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'" ); } return "hasRole('ROLE_" + role + "')" ; }
可以看到,hasRole
的处理逻辑和 hasAuthority
似乎一模一样,但不同的是,hasRole
这里会自动给传入的字符串加上 ROLE_
前缀,所以在数据库中的角色字符串需要加上 ROLE_
前缀。即数据库中存储的用户角色如果是 ROLE_admin
,那对于 Spring Security 来说就是 admin
角色。也就是说,使用 hasAuthority
更具有一致性,不用考虑要不要加 ROLE_
前缀,数据库什么样这里就是什么样的。而 hasRole
则不同,代码里如果写的是 admin
,Spring Security 框架会自动加上 ROLE_
前缀,所以数据库就必须是 ROLE_admin
。
用户授权使用 第一种方式 实现 UserDetailsService
接口,指定用户名、登录密码、权限、角色,并自定义登录页面。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-05
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.2.1.RELEASE</version > <relativePath /> </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > </dependencies >
在 /src/main/resources/static/
目录下,创建 login.html
页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Login Page</title > </head > <body > <div > <form action ="/user/login" method ="post" > 用户名: <input type ="text" name ="username" /> <br /> 密码: <input type ="password" name ="password" /> <br /> <input type ="submit" value ="登录" /> </form > </div > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController public class HelloController { @RequestMapping("/hello") public String hello () { return "Hello Spring Security" ; } @RequestMapping("/goodbye") public String goodbye () { return "Goodbye Spring Security" ; } @RequestMapping("/goodnight") public String goodnight () { return "Goodnight Spring Security" ; } }
实现 UserDetailsService
接口,指定用户名、登录密码、权限、角色。特别注意,这里的角色必须以 ROLE_
前缀开头。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.stereotype.Service;import java.util.List;@Service("userDetailsService") public class LoginServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { List<GrantedAuthority> authors = AuthorityUtils.commaSeparatedStringToAuthorityList("hr,manager,ROLE_sale" ); BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodePassword = passwordEncoder.encode("123456" ); return new User("admin" , encodePassword, authors); } }
创建配置类,指定 UserDetailsService
,并配置自定义的登录页面与访问过滤规则。特别注意,这里的角色不需要以 ROLE_
前缀开头,因为 Spring Security 的底层代码会自动添加前缀与之进行匹配。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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.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.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html" ) .loginProcessingUrl("/user/login" ) .defaultSuccessUrl("/hello" ) .and().authorizeRequests() .antMatchers("/goodnight" ).hasRole("sale" ) .antMatchers("/goodbye" ).hasAuthority("hr" ) .antMatchers("/hello" ).hasAuthority("manager" ) .antMatchers("/" , "/login.html" , "/user/login" ).permitAll() .anyRequest().authenticated() .and().csrf().disable(); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } }
第二种方式 在实现 UserDetailsService
接口的基础上,从数据库中读取用户名、登录密码、权限、角色信息,并自定义登录页面。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-06
。
初始化数据库 特别注意
在数据库中存储的角色都以 ROLE_
前缀开头,如 ROLE_sale
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 CREATE DATABASE `spring_security_study` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE `spring_security_study`; create table user ( id bigint primary key auto_increment, username varchar (20 ) unique not null , password varchar (100 ) ); create table role( id bigint primary key auto_increment, name varchar (20 ), code varchar (20 ) ); create table permission( id bigint primary key auto_increment, name varchar (20 ), url varchar (100 ), parent_id bigint , code varchar (20 ) ); create table user_role( uid bigint , rid bigint ); create table role_permission( rid bigint , pid bigint ); insert into user values (1 , 'wangwu' , '$2a$10$IwvZiSm3vdhRtdyU8rJQz.pb9U/kYHorC2aQqwtFX.RVuFFHOpt82' );insert into user values (2 , 'zhangsan' , '$2a$10$IwvZiSm3vdhRtdyU8rJQz.pb9U/kYHorC2aQqwtFX.RVuFFHOpt82' );insert into role values (1 , '管理员' , 'ROLE_admin' );insert into role values (2 , '销售员' , 'ROLE_sale' );insert into permission values (1 , '系统管理' , '' , 0 , 'system' );insert into permission values (2 , '销售产品' , '' , 0 , 'sale_product' );insert into user_role values (1 , 1 );insert into user_role values (2 , 2 );insert into role_permission values (1 , 1 );insert into role_permission values (1 , 2 );insert into role_permission values (2 , 2 );
引入 Maven 依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.2.1.RELEASE</version > <relativePath /> </parent > <properties > <mybatis-plus.version > 3.0.5</mybatis-plus.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > ${mybatis-plus.version}</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > </dependencies >
创建 Mapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mapper public interface UserMapper extends BaseMapper <User > { List<Role> selectRoleByUserId (@Param("userId") Long userId) ; List<Permission> selectPermissionByUserId (@Param("userId") Long userId) ; }
创建 SQL 映射文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <mapper namespace ="com.clay.security.mapper.UserMapper" > <select id ="selectRoleByUserId" resultType ="com.clay.security.entity.Role" > SELECT r.id, r.name, r.code FROM role r INNER JOIN user_role ru ON r.id = ru.rid where ru.rid = #{userId} </select > <select id ="selectPermissionByUserId" resultType ="com.clay.security.entity.Permission" > SELECT p.id, p.name, p.url, p.parent_id, p.code FROM permission p INNER JOIN role_permission rp ON p.id = rp.pid INNER JOIN role r ON r.id = rp.rid INNER JOIN user_role ru ON r.id = ru.rid where ru.rid = #{userId} </select > </mapper >
实现 UserDetailsService 接口 实现 UserDetailsService
接口,并指定用户名、登录密码、权限、角色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.clay.security.entity.Permission;import com.clay.security.entity.Role;import com.clay.security.entity.User;import com.clay.security.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;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;import java.util.ArrayList;import java.util.List;@Service("userDetailsService") public class LoginServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, username); User userEntity = userMapper.selectOne(queryWrapper); if (userEntity == null ) { throw new UsernameNotFoundException("User name not exist" ); } List<Role> roleList = userMapper.selectRoleByUserId(userEntity.getId()); List<Permission> permissionList = userMapper.selectPermissionByUserId(userEntity.getId()); List<GrantedAuthority> authorList = new ArrayList<>(); roleList.forEach(role -> { authorList.add(new SimpleGrantedAuthority(role.getCode())); }); permissionList.forEach(permission -> { authorList.add(new SimpleGrantedAuthority(permission.getCode())); }); return new org.springframework.security.core.userdetails.User(userEntity.getUsername(), userEntity.getPassword(), authorList); } }
创建核心配置类 创建配置类,指定 UserDetailsService
,并配置自定义的登录页面与访问过滤规则。特别注意,这里的角色不需要以 ROLE_
前缀开头,因为 Spring Security 的底层代码会自动添加前缀与之进行匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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.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.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html" ) .loginProcessingUrl("/user/login" ) .defaultSuccessUrl("/hello" ) .and().authorizeRequests() .antMatchers("/goodnight" ).hasRole("sale" ) .antMatchers("/goodbye" ).hasAuthority("system" ) .antMatchers("/hello" ).hasAuthority("sale_product" ) .antMatchers("/" , "/login.html" , "/user/login" ).permitAll() .anyRequest().authenticated() .and().csrf().disable(); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } }
创建配置文件 这里主要配置数据源和 MyBatis-Plus。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource url: jdbc:mysql://127.0.0.1:3306/spring_security_study?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8 username: root password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml typeAliasesPackage: com.clay.security.**.entity global-config: db-config: id-type: AUTO banner: false configuration: map-underscore-to-camel-case: true call-setters-on-nulls: true jdbc-type-for-null: 'null' log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
用户授权注解 代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-07
。
@Secured 注解 在使用 @Secured
注解之前,必须先使用 @EnableGlobalMethodSecurity(securedEnabled = true)
开启 Spring Security 方法级别的安全功能。
1 2 3 4 5 6 7 8 9 @SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled = true) public class MainApplication { public static void main (String[] args) { SpringApplication.run(MainApplication.class, args); } }
@Secured
注解用于判断当前用户是否拥有角色,在控制器方法上添加 @Secured
注解后,当用户拥有对应的角色才能访问。特别注意,这里的角色必须以 ROLE_
前缀开头。
1 2 3 4 5 6 7 8 9 10 @RestController public class HelloController { @Secured({"ROLE_admin", "ROLE_sale"}) @RequestMapping("/hello") public String hello () { return "Hello Spring Security" ; } }
@PreAuthorize 注解 在使用 @PreAuthorize
注解之前,必须先使用 @EnableGlobalMethodSecurity (prePostEnabled = true)
开启 Spring Security 方法级别的安全功能。
1 2 3 4 5 6 7 8 9 @SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true) public class MainApplication { public static void main (String[] args) { SpringApplication.run(MainApplication.class, args); } }
@PreAuthorize
注解用于判断当前用户是否拥有角色或者权限,适合在方法执行之前进行校验;它可以将登录用户的角色、权限参数传递到表达式中。特别注意,这里的角色必须以 ROLE_
前缀开头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController public class HelloController { @PreAuthorize("hasRole('ROLE_sale')") @RequestMapping("/hello") public String hello () { return "Hello Spring Security" ; } @PreAuthorize("hasAnyRole('ROLE_sale,ROLE_admin')") @RequestMapping("/hello2") public String hello2 () { return "Hello Spring Security" ; } @PreAuthorize("hasAuthority('manager')") @RequestMapping("/goodbye") public String goodbye () { return "Goodbye Spring Security" ; } @PreAuthorize("hasAnyAuthority('manager,hr')") @RequestMapping("/goodbye2") public String goodbye2 () { return "Goodbye Spring Security" ; } }
@PostAuthorize 注解 在使用 @PostAuthorize
注解之前,必须先使用 @EnableGlobalMethodSecurity (prePostEnabled = true)
开启 Spring Security 方法级别的安全功能。
1 2 3 4 5 6 7 8 9 @SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true) public class MainApplication { public static void main (String[] args) { SpringApplication.run(MainApplication.class, args); } }
@PostAuthorize
注解用于判断当前用户是否拥有角色或者权限,适合在方法执行之后进行校验,即适合验证带有返回值的权限;它可以将登录用户的角色、权限参数传递到表达式中。特别注意,这里的角色必须以 ROLE_
前缀开头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController public class HelloController { @PostAuthorize("hasRole('ROLE_sale')") @RequestMapping("/hello") public String hello () { return "Hello Spring Security" ; } @PostAuthorize("hasAnyRole('ROLE_sale,ROLE_admin')") @RequestMapping("/hello2") public String hello2 () { return "Hello Spring Security" ; } @PostAuthorize("hasAuthority('manager')") @RequestMapping("/goodbye") public String goodbye () { return "Goodbye Spring Security" ; } @PostAuthorize("hasAnyAuthority('manager,hr')") @RequestMapping("/goodbye2") public String goodbye2 () { return "Goodbye Spring Security" ; } }
@PostFilter 注解 @PostFilter
注解用于在方法执行之后对数据进行过滤,表达式中的 filterObject
引用的是方法返回值 List
中的某一个元素。在下述的例子中,数据过滤之后只会留下 wangwu
的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController public class HelloController { @PreAuthorize("hasRole('ROLE_sale')") @PostFilter("filterObject.username == 'wangwu'") @RequestMapping("/goodbye") public List<UserInfo> goodbye () { List<UserInfo> list = new ArrayList<>(); list.add(new UserInfo(1L , "wangwu" , "123456" )); list.add(new UserInfo(2L , "zhangsan" , "123456" )); return list; } }
@PreFilter 注解 @PreFilter
注解用于在方法执行之前对数据进行过滤,表达式中的 filterObject
引用的是方法参数 List
中的某一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController public class HelloController { @PreAuthorize("hasRole('ROLE_sale')") @PreFilter(value = "filterObject.id % 2 == 0") @PostMapping("/goodbye") public List<UserInfo> goodbye (@RequestBody List<UserInfo> list) { list.forEach(t -> { System.out.println(t.getId() + " " + t.getUsername()); }); return list; } }
上述代码使用 PostMan 进行测试,数据过滤之后只会留下 wangwu
的数据,如下图所示:
自定义 403 页面 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
在 /src/main/resources/static/
目录下,创建 unauth.html
页面 1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Unauth Page</title > </head > <body > <h2 > 没有权限访问此页面</h2 > </body > </html >
1 2 3 4 5 6 7 8 9 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling().accessDeniedPage("/unauth.html" ); } }