1 2 3 4 5 6
| 作者: 夜泊1990 企鹅: 1611756908 Q 群: 948233848 邮箱: hd1611756908@163.com 博客: https://hs-an-yue.github.io/ B 站: https://space.bilibili.com/514155929/
|
第一章 SpringSecurity概述
1 2 3 4 5 6
| 保护JavaWeb网站安全的框架
环境需求: JDK1.8 SpringBoot2.7.6 版本下的 SpringSecurity版本为 5.7.5 阿里云脚手架
|
第二章 SpringSecurity结构(内部原理)
1
| 了解 SpringSecurity 框架,需要了解JavaWeb的基础知识(Listener、Filter、Servlet) 否则学习 SpringSecurity回很麻烦,根本看不懂。
|
第1节 JavaWeb 内部结构
1 2 3
| 上图是Javaweb的运行原理图,客户端(浏览器或者其他APP)发送请求,先经过多个过滤器,然后最后交给Servlet进行处理
如果看不懂这个就不要往下看了 The End
|
第2节 SpringSecurity 内部结构
1
| SpringSecurity的核心其实就是Filter 下面看一下它的设计演化
|
1 2 3
| 官网地址: https://docs.spring.io/spring-security/reference/5.8/servlet/architecture.html
官网中介绍了 SecurityFilterChain 是SpringSecurity的默认过滤器,这个过滤器提供了拦截规则以及登录逻辑,后面详细介绍
|
第三章 SpringSecurity内置案例
1
| 官方内置了一套简单的验证逻辑,自带登录页面和注销以及内置账户和密码,方便开发者快速入门,本章主要介绍内置案例
|
第1节 环境准备
1 2
| 1. 创建SpringBoot项目,添加依赖 SpringBoot版本采用的 2.7.6 Java开发环境基于JDK1.8,如果想使用更高版本,具体请查看官网
|
1 2
| 3. 项目创建完成后,添加测试代码 HelloController.java 运行项目 通过浏览器 http:
|
第2节 内置案例详细介绍
1 2 3 4 5 6 7 8
| 官网: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/in-memory.html SpringSecurity内置案例核心API
InMemoryUserDetailsManager: 内置账户信息管理类,是UserDetailsService的子类 UserDetailsService: SpringSecurity用户信息管理类的核心接口,管理用户信息来源(数据库还是内存以及其他...) UserDetails: SpringSecurity封装用户信息的核心接口,给SpringSecurity送用户信息时SpringSecurity只认UserDetails
以上的API是内置认证的简单API
|
第3节 修改内置的用户名和密码
1 2 3 4 5 6 7 8 9
| 官网: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/in-memory.html 方式一: API方式(略)
方式二: 配置文件方式 spring: security: user: name: admin password: 123
|
第四章 替换系统自带用户名和密码的获取方式
第1节 认证的运行原理
第2节 替换默认生成的用户信息
2.1 数据库表设计
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
|
CREATE DATABASE rbac DEFAULT CHARACTER SET utf8;
CREATE TABLE user( user_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID主键', phone VARCHAR(50) NOT NULL UNIQUE COMMENT '手机号,唯一', password VARCHAR(255) NOT NULL COMMENT '密码', username VARCHAR(100) NOT NULL COMMENT '用户名' )AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='用户表';
CREATE TABLE role( role_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID主键', role_name VARCHAR(100) NOT NULL COMMENT '角色名' )AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='角色表';
CREATE TABLE permission( permission_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID主键', permission_name VARCHAR(100) NOT NULL COMMENT '权限名' )AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='权限表';
CREATE TABLE user_role( user_id BIGINT NOT NULL COMMENT '用户ID', role_id BIGINT NOT NULL COMMENT '角色ID', PRIMARY KEY (user_id,role_id) ) DEFAULT charset=utf8 COMMENT='用户角色关联表';
CREATE TABLE role_permission( role_id BIGINT NOT NULL COMMENT '角色ID', permission_id BIGINT NOT NULL COMMENT '权限ID', PRIMARY KEY (role_id,permission_id) ) DEFAULT charset=utf8 COMMENT='角色权限关联表';
|
2.2 添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
|
2.3 代码实现
第一步: 配置文件(配置数据库,配置mybatis)
1 2 3 4 5 6 7 8 9 10 11
| spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/rbac?characterEncoding=utf-8 username: root password: root mybatis: mapper-locations: mapper/*.xml type-aliases-package: com.hs.entity configuration: map-underscore-to-camel-case: true
|
第二步: 映射数据库表的实体类
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
|
@Data public class User {
private Long userId;
private String phone;
private String password;
private String username; }
@Data public class Role {
private Long roleId;
private String roleName; }
@Data public class Permission {
private Long permissionId;
private String permissionName; }
@Data public class UserRole {
private Long userId;
private Long roleId; }
@Data public class RolePermission {
private Long roleId;
private Long permissionId; }
|
第三步: mapper层实现使用用户名获取用户信息的函数
1 2 3 4 5 6 7 8 9 10 11 12
|
@Mapper public interface UserMapper {
User getUserByPhone(String phone); }
|
第四步: UserDetails接口实现,封装用户信息(给SpringSecurity送数据)
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
|
@Data public class LoginUserDetails implements UserDetails { private User user; public LoginUserDetails() { } public LoginUserDetails(User user) { this.user = user; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return new ArrayList<>(); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getPhone(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
|
第五步: 替换 InMemoryUserDetailsManager 实现 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
|
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.getUserByPhone(username); if(Objects.isNull(user)){ throw new UsernameNotFoundException(username); }
return new LoginUserDetails(user); } }
|
第五步: 在数据库user表中添加测试账户
第六步: 验证基于数据库的登录是否生效替换了默认用户名和密码的形式
1 2 3 4 5 6 7 8
| 验证是否生效 第一步: 启动项目 第二步: 在浏览器中输入 http://127.0.0.1:8080/test1 服务会自动跳转到SpringSecurity的内置登录页面 第三步: 数据在数据库中自己添加的用户信息 第四步: 查看现象 注意: 肯定会失败,因为密码,因为密码现在在数据库中是明文的,需要设置明文规则 如果在密码前添加{noop},可以不需要进行加密
修改密码后,浏览器中继续测试,就会成功
|
第七步: 如果想数据库中的密码是密文,可以使用 BCryptPasswordEncoder 进行加密和解密
1 2
| 1. 创建配置类 2. 将 BCryptPasswordEncoder 加入到IOC容器中,SpringSecurity 自动生效
|
1
| 编写测试代码,将明文密码加密成密文,保存到数据库中,然后在继续使用浏览器测试.
|
1
| 在浏览器中输入 http://127.0.0.1:8080/test1 会跳转到登录页,在登录页输入 用户名/密码 18233333333/123456 看测试结果
|
第五章 替换页面登录为前后端分离方式
1 2 3 4 5 6 7
| 前后端分离架构是当前最流行的软件架构模式
下面需要修改SpringSecurity默认的逻辑,改成成前后端分离的架构模式。 1. 替换掉登陆页面 2. 修改从内置登录页面获取用户名和密码的逻辑 3. 修改内部默认跳转登录页面的逻辑 4. 修改登录失败的逻辑
|
第0节 前期准备
1 2
| 在设计前后端分离的认证前,需要针对当前项目做一些约束 1. 设计前后端分离架构的统一返回值(无论是成功还是失败,后端给前端返回的数据结构是相同的)
|
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
|
@Setter @Getter public class Result {
private int code=200;
private String msg="成功";
private Object data;
public static Result ok(){ return new Result(); } public static Result ok(Object data){ return new Result(data); } public static Result error(String msg){ return new Result(msg); } public Result() { } public Result(Object data) { this.data = data; } public Result(String msg) { this.code = -1; this.msg = msg; } public Result(int code, String msg) { this.code = code; this.msg = msg; } }
|
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
|
@RestController public class HelloController { @GetMapping(value = "/test1") public Result test1(){ System.out.println("test1..."); return Result.ok("test1..."); } @GetMapping(value = "/test2") public Result test2(){ System.out.println("test2..."); return Result.ok("test2..."); } @GetMapping(value = "/test3") public Result test3(){ System.out.println("test3..."); return Result.ok("test3..."); } }
|
第1节 替换内置登录验证逻辑
1.1 登录验证流程图
1
| 此流程图是基于前后端不分离的表单流程图,和前后端分离的流程基本上一模一样,后面实现前后端分离的登录,参考此流程
|
1.2 登录核心API介绍
1 2 3 4 5 6 7 8 9 10 11
| 根据登录流程图介绍登录相关API
SecurityFilterChain: SpringSecurity核心过滤器,SpringSecurity默认会自动创建一个此对象,用来支持自带的登录,现在采用前后端分离,登录逻辑发生变化,所以需要我们自己创建SpringSecurity来覆盖默认的。
UsernamePasswordAuthenticationToken: 封装前端页面传递过来的用户名和密码,封装好后通过AuthenticationManager传递给SpringSecurity上下文
AuthenticationManager: SpringSecurity的认证管理器,见名知意,用来进行认证
Authentication: 认证实例,认证成功后,里面封装认证成功后的信息
流程图总的其它API暂时使用不上,不做介绍
|
1.3 登录逻辑代码实现
配置认证管理器实例
1 2 3 4 5 6 7
|
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); }
|
配置 SecurityFilterChain
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() return http.build(); }
|
登录逻辑实现
1 2 3
| 官网案例: https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/architecture.html https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/index.html
|
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
| @RestController public class LoginController { @Resource private AuthenticationManager authenticationManager;
@PostMapping(value = "/login") public Result login(String phone,String password){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password); try { Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(Objects.isNull(authenticate)){ return Result.error("认证失败,用户名或密码错误"); } }catch (RuntimeException e){ e.printStackTrace(); return Result.error("认证失败,用户名或密码错误"); } return Result.ok(); } }
|
1
| 代码完成后进行测试,因为没有前端页面所以需要一个客户端工具进行测试,我这里采用postman进行测试
|
1 2 3 4
| 上面是调用登录接口成功和失败的样式
还有另外一种情况,在未登录的情况下,访问了其它资源 例如: http://127.0.0.1:8080/test1 注意: 有人会有疑问,说我前面已经登陆了为什么还不能访问,那是因为前后端分离项目不在是Session-Cookie机制
|
1
| 未登录访问其它资源怎么处理呢?下面介绍 ↓↓↓↓↓↓↓↓
|
第2节 修改未登录访问其它资源的逻辑处理
1 2 3 4 5 6 7 8 9 10
| 实现此功能需要的步骤如下: 第一步: 添加依赖(json工具依赖) <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.32</version> </dependency> 第二步: 实现AuthenticationEntryPoint接口,实现自定义的处理器 第三步: 将自定义的处理器注册到Spring Security的核心Filter中 第四步: 测试是否生效
|
第二步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Component public class LoginUnAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); Result result = Result.error("用户未认证或登录已过期,请重新登录后再访问"); String json = JSONUtil.toJsonStr(result); response.getWriter().print(json); } }
|
第三步
1
| 将自定义的处理器注册到Spring Security的核心过滤器中
|
第四步
第3节 解决HTTP协议无状态
1 2 3 4 5
| 传统前后端不分离的架构采用Session+Cookie机制
现在前后端分离架构,采用token令牌方式
token生成采用JWT工具生成和校验,使用Redis数据库进行保存
|
3.1 Redis数据库安装和启动
3.2 JWT工具类编写
添加依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
工具类代码实现
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
|
public class JwtUtils { private static Algorithm hmac256 = Algorithm.HMAC256("YLWTSMTJFYHDCMGSCWHSSYBZSDKC");
public static String sign(String pub, Long expiresTime){ return JWT.create() .withIssuer(pub) .withExpiresAt(new Date(System.currentTimeMillis()+expiresTime)) .sign(hmac256); }
public static boolean verify(String token){ JWTVerifier verifier = JWT.require(hmac256).build(); verifier.verify(token); return true; }
public static String getClaim(String token){ DecodedJWT jwt = JWT.decode(token); Claim iss = jwt.getClaim("iss"); return iss.asString(); } }
|
3.3 Redis客户端实现
配置文件修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/rbac?characterEncoding=utf-8 username: root password: root redis: host: 127.0.0.1 password: database: 0 mybatis: mapper-locations: mapper/*.xml type-aliases-package: com.hs.entity configuration: map-underscore-to-camel-case: true
|
代码实现
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
|
@Component public class RedisClient { @Resource private StringRedisTemplate stringRedisTemplate;
public void set (String key,String value){ stringRedisTemplate.opsForValue().set(key,value); }
public void set (String key,String value,Long time){ stringRedisTemplate.opsForValue().set(key,value,time, TimeUnit.MILLISECONDS); }
public String get(String key){ return stringRedisTemplate.opsForValue().get(key); }
public void del(String key){ stringRedisTemplate.delete(key); }
public Boolean exists(String key){ return stringRedisTemplate.hasKey(key); } }
|
3.4 登录逻辑修改
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
| @RestController public class LoginController { @Resource private AuthenticationManager authenticationManager; @Resource private RedisClient redisClient;
@PostMapping(value = "/login") public Result login(String phone,String password){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password); try { Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(Objects.isNull(authenticate)){ return Result.error("认证失败,用户名或密码错误"); } LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal(); if(Objects.isNull(principal)){ return Result.error("认证失败,用户名或密码错误"); } String json = JSONUtil.toJsonStr(principal); String token = JwtUtils.sign(principal.getUsername(), 1000 * 60 * 60 * 24 * 7L); redisClient.set("login:token:"+token,json,1000*60 * 60 * 24 * 7L); Map<String,Object> map = new HashMap<>(); map.put("token",token); return Result.ok(map); }catch (RuntimeException e){ e.printStackTrace(); return Result.error("认证失败,用户名或密码错误"); } } }
|
3.5 其它资源访问基于token令牌方式
1
| 使用令牌方式,替换前后端不分离的session-cookie机制,解决HTTP无状态的问题
|
SpringSecurity运行原理
1 2 3 4 5 6
| 简单了解一下SpringSecurity的功能实现 SpringSecurity的核心是Filter(过滤器),核心功能实现也是由一个个过滤器组成,详情请查看官网
官网:https://docs.spring.io/spring-security/reference/5.8/servlet/architecture.html
下面是SpringSecurity内置的过滤器
|
1 2 3 4
| 简单画了一个SpringSecurity原理图,客戶端发送过来的请求会经过一个个的过滤器,每一个过滤器承担着不同的功能 例如UsernamePasswordAuthenticationFilter过滤器帮助我们校验账户和密码
现在我们要模拟session+cookie机制,通过token凭据实现权限控制,方式如下
|
基于token机制的实现
1 2
| 第一步: 自定义 Filter 第二步: 注册到SpringSecurity的Filter中
|
第一步
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
|
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Resource private RedisClient redisClient; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token"); if(StringUtils.hasLength(token)){ String key = "login:token:"+token; String json = redisClient.get(key); if(StringUtils.hasLength(json)){ LoginUserDetails user = JSONUtil.toBean(json, LoginUserDetails.class); if(Objects.nonNull(user)){ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); }else { SecurityContextHolder.getContext().setAuthentication(null); } } } filterChain.doFilter(request,response); } }
|
第二步
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
|
@Configuration public class SecurityConfig {
@Resource private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler; @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean public PasswordEncoder generalPasswordEncoder(){ return new BCryptPasswordEncoder(); }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler); return http.build(); }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } }
|
测试验证
3.6 反复登录,多token解决
1 2 3
| 客户端发送的所有请求都需要带token,在进行登录时单独进行token的校验,如果登陆过,刷新token
在登录中添加一个校验逻辑,删除原来的key
|
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 56 57 58 59 60 61 62 63 64 65 66 67 68
| @RestController public class LoginController { @Resource private AuthenticationManager authenticationManager; @Resource private RedisClient redisClient;
@PostMapping(value = "/login") public Result login(String phone, String password, HttpServletRequest request){
String token_ = request.getHeader("token"); if(StringUtils.hasText(token_)){ String claim = JwtUtils.getClaim(token_); if(StringUtils.hasText(claim)){ if(phone.equals(claim)){ String key="login:token:"+token_; redisClient.del(key); } } }
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password); try { Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(Objects.isNull(authenticate)){ return Result.error("认证失败,用户名或密码错误"); } LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal(); if(Objects.isNull(principal)){ return Result.error("认证失败,用户名或密码错误"); } String json = JSONUtil.toJsonStr(principal); String token = JwtUtils.sign(principal.getUsername(), 1000 * 60 * 60 * 24 * 7L); redisClient.set("login:token:"+token,json,1000*60 * 60 * 24 * 7L); Map<String,Object> map = new HashMap<>(); map.put("token",token); return Result.ok(map); }catch (RuntimeException e){ e.printStackTrace(); return Result.error("认证失败,用户名或密码错误"); } } }
|
第六章 授权
1
| 授权的意义就是当一个用户登录成功后,此账户本身拥有的权限(例如 当前用户用哪些角色,当前角色都能干什么[删除|更新等])
|
第1节 将权限从数据库送到SpringSecurity上下文
1 2 3 4 5 6 7 8 9 10 11
| 从数据库中将用户信息送到上下文 在 UserDetailsService接口的实现类中
实现步骤: 第一步: mapper提供查询方法,将用户相关的权限信息查询到封装到UserDetails的对象中 通过用户ID查询角色名称列表 通过角色ID查询权限名称列表 第二步: 在UserDetailsService中进行封装 在UserDetailsService中调用mapper并且封装传递给 SpringSecurity 上下文,修改UserDetails结构添加属性等 第三步: 在控制器层进行注解控制(配置文件) 第四步: 开启注解配置否则不生效 第五步: 权限不够的处理器
|
1.1 第一步
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
|
@Mapper public interface UserRoleMapper {
List<UserRole> getUserRolesByUserId(Long userId); }
@Mapper public interface RoleMapper {
List<Role> batchGetRolesByRoleIds(List<Long> roleIds); }
@Mapper public interface RolePermissionMapper {
List<RolePermission> getRolePermissionsByRoleIds(List<Long> roleIds); }
@Mapper public interface PermissionMapper {
List<Permission> batchGetPermissionsByPermissionIds(List<Long> permissionIds); }
|
1.2 第二步
UserDetails 修改
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
@Data public class LoginUserDetails implements UserDetails {
private User user;
private List<String> roleNames; private List<String> permissionNames;
public LoginUserDetails(User user, List<String> roleNames, List<String> permissionNames) { this.user = user; this.roleNames = roleNames; this.permissionNames = permissionNames; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); if(!CollectionUtils.isEmpty(roleNames)){ for (String roleName : roleNames) { authorities.add(new SimpleGrantedAuthority("ROLE_"+roleName)); } } if(!CollectionUtils.isEmpty(permissionNames)){ for (String permissionName : permissionNames) { authorities.add(new SimpleGrantedAuthority(permissionName)); } } return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getPhone(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
|
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 56 57 58 59 60 61 62 63 64
|
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Resource private UserRoleMapper userRoleMapper; @Resource private RoleMapper roleMapper; @Resource private RolePermissionMapper rolePermissionMapper; @Resource private PermissionMapper permissionMapper;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.getUserByPhone(username); if(Objects.isNull(user)){ throw new UsernameNotFoundException(username); }
List<UserRole> userRoles = userRoleMapper.getUserRolesByUserId(user.getUserId()); List<String> roleNames = new ArrayList<>(); List<String> permissionNames = new ArrayList<>(); if(!CollectionUtils.isEmpty(userRoles)){ List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList()); if(!CollectionUtils.isEmpty(roleIds)){ List<Role> roles = roleMapper.batchGetRolesByRoleIds(roleIds); if(!CollectionUtils.isEmpty(roles)){ List<String> roleNameList = roles.stream().map(Role::getRoleName).collect(Collectors.toList()); roleNames.addAll(roleNameList); } List<RolePermission> rolePermissions = rolePermissionMapper.getRolePermissionsByRoleIds(roleIds); if(!CollectionUtils.isEmpty(rolePermissions)){ List<Long> permissionIdList = rolePermissions.stream().map(RolePermission::getPermissionId).collect(Collectors.toList()); if(!CollectionUtils.isEmpty(permissionIdList)){ List<Permission> permissions = permissionMapper.batchGetPermissionsByPermissionIds(permissionIdList); if(!CollectionUtils.isEmpty(permissions)){ List<String> permissionNameList = permissions.stream().map(Permission::getPermissionName).collect(Collectors.toList()); if(!CollectionUtils.isEmpty(permissionNameList)){ permissionNames.addAll(permissionNameList); } } } } } } return new LoginUserDetails(user,roleNames,permissionNames); } }
|
1.3 第三步
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
|
@RestController public class HelloController {
@GetMapping(value = "/test1") public Result test1(){ System.out.println("test1..."); return Result.ok("test1..."); }
@GetMapping(value = "/test2") public Result test2(){ System.out.println("test2..."); return Result.ok("test2..."); }
@PreAuthorize(value = "hasRole('admin')") @GetMapping(value = "/test3") public Result test3(){ System.out.println("test3..."); return Result.ok("test3..."); }
@PreAuthorize(value = "hasAnyRole('admin','CEO')") @GetMapping(value = "/test4") public Result test4(){ System.out.println("test4..."); return Result.ok("test4..."); }
@PreAuthorize(value = "hasRole('CTO') and hasRole('CEO')") @GetMapping(value = "/test5") public Result test5(){ System.out.println("test5..."); return Result.ok("test5..."); }
@PreAuthorize(value = "hasAuthority('user:add')") @GetMapping(value = "/test6") public Result test6(){ System.out.println("test6..."); return Result.ok("test6..."); }
@PreAuthorize(value = "hasAnyAuthority('user:add','user:del')") @GetMapping(value = "/test7") public Result test7(){ System.out.println("test7..."); return Result.ok("test7..."); }
@PreAuthorize(value = "hasAuthority('user:add') and hasAuthority('user:del')") @GetMapping(value = "/test8") public Result test8(){ System.out.println("test8..."); return Result.ok("test8..."); } }
|
1.4 第四步
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
|
@Configuration @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig {
@Resource private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler; @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Resource private LoginUnAccessDeniedHandler loginUnAccessDeniedHandler;
@Bean public PasswordEncoder generalPasswordEncoder(){ return new BCryptPasswordEncoder(); }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login","/test1").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler); http.exceptionHandling().accessDeniedHandler(loginUnAccessDeniedHandler); return http.build(); }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } }
|
1.5 第五步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Component public class LoginUnAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); Result result = Result.error("权限不足,请重新授权。"); String json = JSONUtil.toJsonStr(result); response.getWriter().print(json); } }
|
第七章 注销
1 2
| 注销比较简单,直接将redis数据清空了即可 SpringSecurity内置注销功能,咱们使用内置的注销,覆盖注销的逻辑即可
|
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
@Component public class LogoutStatusSuccessHandler implements LogoutSuccessHandler { @Resource private RedisClient redisClient;
@Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String token = request.getHeader("token"); if(StringUtils.hasText(token)){ redisClient.del("login:token:"+token); } response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); Result result = Result.ok(200,"注销成功"); String json = JSONUtil.toJsonStr(result); response.getWriter().print(json); } }
@Configuration @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig { @Resource private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler; @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Resource private LoginUnAccessDeniedHandler loginUnAccessDeniedHandler; @Resource private LogoutStatusSuccessHandler logoutStatusSuccessHandler;
@Bean public PasswordEncoder generalPasswordEncoder(){ return new BCryptPasswordEncoder(); }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login","/logout","/test1").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler); http.exceptionHandling().accessDeniedHandler(loginUnAccessDeniedHandler); http.logout().logoutSuccessHandler(logoutStatusSuccessHandler); return http.build(); }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } }
|
第八章 总结
1
| 以上就是SpringSecurity的核心知识点,没什么好总结的,一切从官网出发. 你学废了吗 !!!!
|