• SpringSecurity学习 - 认证和授权


    一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

    一般Web应用的需要进行认证授权

    认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

    授权:经过认证后判断当前用户是否有权限进行某个操作

    而认证和授权也是SpringSecurity作为安全框架的核心功能。

    我所学习的正是,认证和授权和流程。

    另外默认SpringBoot、Redis会调用。

    1.简单入门Demo

    新建一个SpringBoot工程。这是使用的依赖。

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-securityartifactId>
    4. dependency>
    5. <dependency>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-webartifactId>
    8. dependency>
    9. <dependency>
    10. <groupId>org.springframework.bootgroupId>
    11. <artifactId>spring-boot-starter-testartifactId>
    12. <scope>testscope>
    13. dependency>
    14. <dependency>
    15. <groupId>org.springframework.bootgroupId>
    16. <artifactId>spring-boot-starter-data-redisartifactId>
    17. dependency>
    18. <dependency>
    19. <groupId>com.baomidougroupId>
    20. <artifactId>mybatis-plus-boot-starterartifactId>
    21. <version>3.5.3.1version>
    22. dependency>
    23. <dependency>
    24. <groupId>mysqlgroupId>
    25. <artifactId>mysql-connector-javaartifactId>
    26. dependency>
    27. <dependency>
    28. <groupId>org.projectlombokgroupId>
    29. <artifactId>lombokartifactId>
    30. dependency>
    31. <dependency>
    32. <groupId>com.alibabagroupId>
    33. <artifactId>fastjsonartifactId>
    34. <version>1.2.58version>
    35. dependency>
    36. <dependency>
    37. <groupId>com.fasterxml.jackson.datatypegroupId>
    38. <artifactId>jackson-datatype-jdk8artifactId>
    39. dependency>
    40. <dependency>
    41. <groupId>io.jsonwebtokengroupId>
    42. <artifactId>jjwtartifactId>
    43. <version>0.9.1version>
    44. dependency>
    45. <dependency>
    46. <groupId>org.apache.commonsgroupId>
    47. <artifactId>commons-pool2artifactId>
    48. dependency>
    49. <dependency>
    50. <groupId>javax.xml.bindgroupId>
    51. <artifactId>jaxb-apiartifactId>
    52. <version>2.3.0version>
    53. dependency>
    54. <dependency>
    55. <groupId>com.sun.xml.bindgroupId>
    56. <artifactId>jaxb-implartifactId>
    57. <version>2.3.0version>
    58. dependency>
    59. <dependency>
    60. <groupId>com.sun.xml.bindgroupId>
    61. <artifactId>jaxb-coreartifactId>
    62. <version>2.3.0version>
    63. dependency>
    64. <dependency>
    65. <groupId>javax.activationgroupId>
    66. <artifactId>activationartifactId>
    67. <version>1.1.1version>
    68. dependency>

    启动之后,访问localhost:8888, 出现一下页面代表SpringSecurity生效了。

     此时我们发现控制台生成:

     我们向登录表单输入,这段密码和用户名user,即可登录通过。

     2.认证

    2.1 登录校验流程

     当然,我们实际情况,可能会加一个redis做缓存,登录之后,用用户id -> 用户信息,存储到redis中。

    这样我们登录之后,解析token,就是从redis查询。

    2.2 SpringSecurity验证流程

    图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

    UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

    ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

    FilterSecurityInterceptor:负责权限校验的过滤器。

    2.3 认证流程详解

    Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

    AuthenticationManager接口:定义了认证Authentication的方法

    UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

    UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

    2.4 思路分析

    1. ...,在现在前后端分离项目,我们肯定是要自定义登录接口,不能用SpringSecurity的登录页面;

    2. UserDetailService 是在内存中查找比较用户输入的用户名和密码,那我们需要是需要查询数据库比对的。

    2.5 问题解决

    2.5.0 前提

    user.sql

    1. DROP TABLE IF EXISTS `user`;
    2. CREATE TABLE `user` (
    3. `id` bigint NOT NULL COMMENT '主键',
    4. `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
    5. `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
    6. `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
    7. `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
    8. `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
    9. `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
    10. `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
    11. `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
    12. `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
    13. `create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
    14. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
    15. `update_by` bigint DEFAULT NULL COMMENT '更新人',
    16. `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    17. `del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
    18. PRIMARY KEY (`id`)
    19. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

     application.yaml 中数据库配置

    1. spring:
    2. datasource:
    3. url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
    4. username: root
    5. password: root
    6. driver-class-name: com.mysql.cj.jdbc.Driver

    UserMapper

    1. @Mapper
    2. public interface UserMapper extends BaseMapper {
    3. }

    User

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. @TableName(value = "user")
    5. public class User {
    6. /**
    7. * 主键
    8. */
    9. @TableId
    10. private Long id;
    11. /**
    12. * 用户名
    13. */
    14. private String userName;
    15. /**
    16. * 昵称
    17. */
    18. private String nickName;
    19. /**
    20. * 密码
    21. */
    22. private String password;
    23. /**
    24. * 账号状态(0正常 1停用)
    25. */
    26. private String status;
    27. /**
    28. * 邮箱
    29. */
    30. private String email;
    31. /**
    32. * 手机号
    33. */
    34. private String phonenumber;
    35. /**
    36. * 用户性别(0男,1女,2未知)
    37. */
    38. private String sex;
    39. /**
    40. * 头像
    41. */
    42. private String avatar;
    43. /**
    44. * 用户类型(0管理员,1普通用户)
    45. */
    46. private String userType;
    47. /**
    48. * 创建人的用户id
    49. */
    50. private Long createBy;
    51. /**
    52. * 创建时间
    53. */
    54. private Date createTime;
    55. /**
    56. * 更新人
    57. */
    58. private Long updateBy;
    59. /**
    60. * 更新时间
    61. */
    62. private Date updateTime;
    63. /**
    64. * 删除标志(0代表未删除,1代表已删除)
    65. */
    66. private Integer delFlag;
    67. }

    UserDetailServiceImpl

    1. @Data
    2. @NoArgsConstructor
    3. @AllArgsConstructor
    4. public class UserDetailsImpl implements UserDetails {
    5. private User user;
    6. //private List authList;
    7. //
    8. //@JSONField(serialize = false)
    9. //private List authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis
    10. //
    11. //
    12. //public UserDetailsImpl(User user, List authList) { // 将对应的权限字符串列表传入
    13. // this.user = user;
    14. // this.authList = authList;
    15. //}
    16. //
    17. @Override
    18. public Collectionextends GrantedAuthority> getAuthorities() {
    19. 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
    20. //if(authorities != null){
    21. // return authorities;
    22. //}else{
    23. // authorities = new ArrayList<>();
    24. //}
    25. //
    26. 第一次登录,封装UserDetails对象,初始化权限列表
    27. //for (String auth : authList) {
    28. // SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
    29. // authorities.add(simpleGrantedAuthority); // 对,默认是个空的
    30. //}
    31. //return authorities;
    32. return null;
    33. }
    34. @Override
    35. public String getPassword() {
    36. return user.getPassword();
    37. }
    38. @Override
    39. public String getUsername() {
    40. return user.getUserName();
    41. }
    42. @Override
    43. public boolean isAccountNonExpired() { // 下面bool值,全部响应为true,UserDetail对象返回校验过程中,会因为没权限报错
    44. return true;
    45. }
    46. @Override
    47. public boolean isAccountNonLocked() {
    48. return true;
    49. }
    50. @Override
    51. public boolean isCredentialsNonExpired() {
    52. return true;
    53. }
    54. @Override
    55. public boolean isEnabled() {
    56. return true;
    57. }
    58. }

    RedisConfig : redis配置类

    1. @Configuration
    2. public class RedisConfig {
    3. @Bean
    4. @SuppressWarnings(value = { "unchecked", "rawtypes" })
    5. public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    6. {
    7. RedisTemplate template = new RedisTemplate<>();
    8. template.setConnectionFactory(connectionFactory);
    9. FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
    10. // 使用StringRedisSerializer来序列化和反序列化redis的key值
    11. template.setKeySerializer(new StringRedisSerializer());
    12. template.setValueSerializer(serializer);
    13. // Hash的key也采用StringRedisSerializer的序列化方式
    14. template.setHashKeySerializer(new StringRedisSerializer());
    15. template.setHashValueSerializer(serializer);
    16. template.afterPropertiesSet();
    17. return template;
    18. }
    19. }

     Result : 后端统一封装响应

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. public class Result {
    5. private T data;
    6. private String mes;
    7. private Integer code;
    8. public Result(String mes, Integer _code) {
    9. }
    10. public static Result success(T data){
    11. return new Result(data,"操作成功",200);
    12. }
    13. public static Result success(String _mes){
    14. return new Result(_mes,200);
    15. }
    16. public static Result error(String _mes,Integer _code){
    17. return new Result(_mes,_code);
    18. }
    19. }

     JwtUtil 工具类

    1. /**
    2. * JWT工具类
    3. */
    4. public class JwtUtil {
    5. //有效期为
    6. public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
    7. //设置秘钥明文
    8. public static final String JWT_KEY = "sangeng";
    9. public static String getUUID(){
    10. String token = UUID.randomUUID().toString().replaceAll("-", "");
    11. return token;
    12. }
    13. /**
    14. * 生成jtw
    15. * @param subject token中要存放的数据(json格式)
    16. * @return
    17. */
    18. public static String createJWT(String subject) {
    19. JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
    20. return builder.compact();
    21. }
    22. /**
    23. * 生成jtw
    24. * @param subject token中要存放的数据(json格式)
    25. * @param ttlMillis token超时时间
    26. * @return
    27. */
    28. public static String createJWT(String subject, Long ttlMillis) {
    29. JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
    30. return builder.compact();
    31. }
    32. private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
    33. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    34. SecretKey secretKey = generalKey();
    35. long nowMillis = System.currentTimeMillis();
    36. Date now = new Date(nowMillis);
    37. if(ttlMillis==null){
    38. ttlMillis=JwtUtil.JWT_TTL;
    39. }
    40. long expMillis = nowMillis + ttlMillis;
    41. Date expDate = new Date(expMillis);
    42. return Jwts.builder()
    43. .setId(uuid) //唯一的ID
    44. .setSubject(subject) // 主题 可以是JSON数据
    45. .setIssuer("sg") // 签发者
    46. .setIssuedAt(now) // 签发时间
    47. .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
    48. .setExpiration(expDate);
    49. }
    50. /**
    51. * 创建token
    52. * @param id
    53. * @param subject
    54. * @param ttlMillis
    55. * @return
    56. */
    57. public static String createJWT(String id, String subject, Long ttlMillis) {
    58. JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
    59. return builder.compact();
    60. }
    61. public static void main(String[] args) throws Exception {
    62. // String jwt = createJWT("2123");
    63. Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
    64. String subject = claims.getSubject();
    65. System.out.println(subject);
    66. // System.out.println(claims);
    67. }
    68. /**
    69. * 生成加密后的秘钥 secretKey
    70. * @return
    71. */
    72. public static SecretKey generalKey() {
    73. byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
    74. SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    75. return key;
    76. }
    77. /**
    78. * 解析
    79. *
    80. * @param jwt
    81. * @return
    82. * @throws Exception
    83. */
    84. public static Claims parseJWT(String jwt) throws Exception {
    85. SecretKey secretKey = generalKey();
    86. return Jwts.parser()
    87. .setSigningKey(secretKey)
    88. .parseClaimsJws(jwt)
    89. .getBody();
    90. }
    91. }
    FastJsonRedisSerializer 
    1. public class FastJsonRedisSerializer implements RedisSerializer
    2. {
    3. public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    4. private Class clazz;
    5. static
    6. {
    7. ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    8. }
    9. public FastJsonRedisSerializer(Class clazz)
    10. {
    11. super();
    12. this.clazz = clazz;
    13. }
    14. @Override
    15. public byte[] serialize(T t) throws SerializationException
    16. {
    17. if (t == null)
    18. {
    19. return new byte[0];
    20. }
    21. return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    22. }
    23. @Override
    24. public T deserialize(byte[] bytes) throws SerializationException
    25. {
    26. if (bytes == null || bytes.length <= 0)
    27. {
    28. return null;
    29. }
    30. String str = new String(bytes, DEFAULT_CHARSET);
    31. return JSON.parseObject(str, clazz);
    32. }
    33. protected JavaType getJavaType(Class clazz)
    34. {
    35. return TypeFactory.defaultInstance().constructType(clazz);
    36. }
    37. }

    2.5.1 自定义UserDetailService

    我们自定义UserDatailService 实现UserDatailService接口,注入到spring容器中,这样就会在SpringSecurity的认证流程中调用我们自定义的实现类。

    1. @Service
    2. public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
    3. @Override
    4. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    5. // 用用户名查询对应User
    6. LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
    7. queryWrapper.eq(User::getUserName,username);
    8. // 判断是否空
    9. User user = this.getOne(queryWrapper);
    10. if(user == null){
    11. throw new RuntimeException("用户名或密码错误");
    12. }
    13. // 将查询到user封装到自定义UserDeatail中
    14. return new UserDetailsImpl(user);
    15. }
    16. }

    我们看下图,我们输入的用户名和密码,最终会传入UserDetailService 对象,加载loadUserByUsername 方法,返回UserDtail对象。返回过程中,会与UserDetail中的password和username进行比对,不同则报错。

    注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如 : 数据库中,username:sg, password: {noop}1234

    这样登陆的时候就可以用sg作为用户名,1234作为密码来登陆了。

    2.5.2 密码加密存储

    在实际中项目中,我一般不会明文存储,而是采用PasswordEncoder加密的方式;

    我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。

    我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验

    1. @Configuration
    2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    3. @Bean
    4. public PasswordEncoder passwordEncoder(){
    5. return new BCryptPasswordEncoder();
    6. }
    7. }

    2.5.3 自定义登录接口

    • 接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
    • 在接口中我们通过AuthenticationManagerauthenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
    • 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

    UserController 

    1. @RestController
    2. @RequestMapping("/user")
    3. public class UserController {
    4. @Autowired
    5. private UserService userService;
    6. @PostMapping("/login")
    7. Result login(@RequestBody User user){
    8. return userService.login(user);
    9. }
    10. }
    1. public interface UserService extends IService {
    2. Result login(User user);
    3. }
    1. @Service
    2. public class UserServiceImpl extends ServiceImpl implements UserService {
    3. @Autowired
    4. private AuthenticationManager authenticationManager;
    5. @Autowired
    6. private RedisTemplate redisTemplate;
    7. @Override
    8. public Result login(User user) {
    9. // AuthenticationManager authenticationManager 进行认证
    10. UsernamePasswordAuthenticationToken authenticationToken =
    11. new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
    12. Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    13. // 如果认证通过、给出对应提示
    14. if(Objects.isNull(authenticate)){
    15. throw new RuntimeException("登录失败");
    16. }
    17. // 认证通过使用userid生成jwt
    18. UserDetailsImpl udi = (UserDetailsImpl) authenticate.getPrincipal();
    19. String userId = udi.getUser().getId().toString();
    20. String token = JwtUtil.createJWT(userId);
    21. HashMap map = new HashMap<>();
    22. map.put("token",token);
    23. // 把完整用户信息存入redis
    24. redisTemplate.opsForValue().set("login:" +userId,udi);
    25. return Result.success(map,"登录成功");
    26. }
    27. }

    SpringSecurityConfig

    1. @Configuration
    2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    3. @Bean
    4. public PasswordEncoder passwordEncoder(){
    5. return new BCryptPasswordEncoder();
    6. }
    7. @Override
    8. protected void configure(HttpSecurity http) throws Exception {
    9. http
    10. //关闭csrf
    11. .csrf().disable()
    12. //不通过Session获取SecurityContext
    13. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    14. .and()
    15. .authorizeRequests()
    16. // 对于登录接口 允许匿名访问
    17. .antMatchers("/user/login").anonymous()
    18. // 除上面外的所有请求全部需要鉴权认证
    19. .anyRequest().authenticated();
    20. }
    21. @Bean
    22. @Override
    23. public AuthenticationManager authenticationManagerBean() throws Exception {
    24. return super.authenticationManagerBean();
    25. }
    26. }
    1. spring:
    2. # mysql配置
    3. datasource:
    4. url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
    5. username: root
    6. password: 123456
    7. driver-class-name: com.mysql.cj.jdbc.Driver
    8. # redis配置
    9. redis:
    10. # 默认0
    11. database: 0
    12. #连接超时时间
    13. timeout: 10000ms
    14. port: 6379
    15. host: 192.168.213.136
    16. lettuce:
    17. pool:
    18. # 设置最大连接数
    19. max-active: 1024
    20. # 最大阻塞时间
    21. max-wait: 10000ms
    22. # 最大空间连接,默认8
    23. max-idle: 200
    24. # 最小空间连接,默认5
    25. min-idle: 5
    26. server:
    27. port: 8888

     2.5.4 自定义jwt过滤器

    以下跨域设置,postman测试是完全没有问题的。但是,我们是为了前后端分离,会有问题。

    当登录之后,携带token的请求头,被jwt过滤器捕获解析之后,获得userId,用userId将从redis拿出UserDetailImpl封装到 SecurityContextHolder.getContext()中,被SpringSecurity过滤链捕获到,证明没有问题,因此放行访问资源。

    1. /**
    2. * jwt过滤器
    3. *
    4. * @author: qhx20040819
    5. * @date: 2023-09-09 21:27
    6. **/
    7. @Component
    8. public class JwtFilter extends OncePerRequestFilter {
    9. @Autowired
    10. private RedisTemplate redisTemplate;
    11. @Override
    12. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    13. // 设置跨域
    14. response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PS
    15. response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT");
    16. response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS
    17. // 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。
    18. response.setHeader("Access-Control-Max-Age", "3600");
    19. //获取token
    20. String authorization = request.getHeader("Authorization");
    21. if(StringUtils.isEmpty(authorization)){
    22. filterChain.doFilter(request,response);
    23. return;
    24. }
    25. String token = authorization.substring(6);
    26. //解析token
    27. String userid;
    28. try {
    29. Claims claims = JwtUtil.parseJWT(token);
    30. userid = claims.getSubject();
    31. } catch (Exception e) {
    32. e.printStackTrace();
    33. throw new RuntimeException("token非法");
    34. }
    35. //从redis中获取用户信息
    36. String redisKey = "login:" + userid;
    37. UserDetailsImpl udi = (UserDetailsImpl) redisTemplate.opsForValue().get(redisKey);
    38. if(Objects.isNull(udi)){
    39. throw new RuntimeException("用户未登录");
    40. }
    41. //存入SecurityContextHolder
    42. //获取权限信息封装到Authentication中
    43. UsernamePasswordAuthenticationToken authenticationToken = // 现在权限字段是null
    44. new UsernamePasswordAuthenticationToken(udi,null,udi.getAuthorities());
    45. SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    46. //放行
    47. filterChain.doFilter(request, response);
    48. }
    49. }

    3.授权

    3.1 授权流程

    在SpringSecurity中,会使用默认FiterSecuritInterceptor来进行权限校验,而它又是从SpringSecurityContex中Authentication,来获取其中的权限信息。判断当前用户是否拥有访问当前资源权限。

    因此,我们只需要将权限信息存储到Authentication中即可,设置资源的访问权限。

    3.2 问题解决

    3.2.1 相关配置

    用注解开启相关配置。

    @EnableGlobalMethodSecurity(prePostEnabled = true)

     在相应的资源上开启访问权限管控。

    1. @RequestMapping("/hello")
    2. @PreAuthorize("hasAnyAuthority('test')") // 限制访问权限字段
    3. public String hello(){
    4. return "hello";
    5. }

    3.2.2 封装权限字段

    我们之前UserDetailServiceImpl中loadUserByUsername()中,返回的UserDetailImpl对象,我们之前创建时,并没传入权限字段,我们先自定义权限字段模拟用户权限列表。

    1. @Service
    2. public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
    3. @Override
    4. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    5. // 用用户名查询对应User
    6. LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
    7. queryWrapper.eq(User::getUserName,username);
    8. // 判断是否空
    9. User user = this.getOne(queryWrapper);
    10. if(user == null){
    11. throw new RuntimeException("用户名或密码错误");
    12. }
    13. List authlist = new ArrayList<>(); // 暂且封装这样权限字段
    14. authlist.add("test");
    15. authlist.add("test0");
    16. return new UserDetailsImpl(user,authlist);
    17. }
    18. }

    相应的UserDetailImpl中也需要修改,seucrity过滤器链是从getAuthorities()方法中来获取用户权限字段的。

    1. @Data
    2. @NoArgsConstructor
    3. @AllArgsConstructor
    4. public class UserDetailsImpl implements UserDetails {
    5. private User user;
    6. private List authList;
    7. @JSONField(serialize = false)
    8. private List authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis
    9. public UserDetailsImpl(User user, List authList) { // 将对应的权限字符串列表传入
    10. this.user = user;
    11. this.authList = authList;
    12. }
    13. @Override
    14. public Collectionextends GrantedAuthority> getAuthorities() {
    15. // 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
    16. if(authorities != null){
    17. return authorities;
    18. }else{
    19. authorities = new ArrayList<>();
    20. }
    21. // 第一次登录,封装UserDetails对象,初始化权限列表
    22. for (String auth : authList) {
    23. SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
    24. authorities.add(simpleGrantedAuthority); // 对,默认是个空的
    25. }
    26. return authorities;
    27. }
    28. @Override
    29. public String getPassword() {
    30. return user.getPassword();
    31. }
    32. @Override
    33. public String getUsername() {
    34. return user.getUserName();
    35. }
    36. @Override
    37. public boolean isAccountNonExpired() {
    38. return true;
    39. }
    40. @Override
    41. public boolean isAccountNonLocked() {
    42. return true;
    43. }
    44. @Override
    45. public boolean isCredentialsNonExpired() {
    46. return true;
    47. }
    48. @Override
    49. public boolean isEnabled() {
    50. return true;
    51. }
    52. }

    这是拿着携带token去请求,能访问成功。

     3.2.3 数据库查询权限字段

    就是我们真实的权限应该封装在数据库中。

    3.2.3.1 RBAC权限模型

    一个用户,可以对应多个角色;一个角色,可以对应多个用户;

    一个角色,可以拥有多个权限字段; 一个权限字段,可以被多个角色所拥有;

     3.2.3.2 准备工作
    1. USE `test`;
    2. /*Table structure for table `menu` */
    3. DROP TABLE IF EXISTS `menu`;
    4. CREATE TABLE `menu` (
    5. `id` bigint(20) NOT NULL AUTO_INCREMENT,
    6. `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
    7. `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
    8. `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
    9. `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
    10. `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
    11. `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
    12. `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
    13. `create_by` bigint(20) DEFAULT NULL,
    14. `create_time` datetime DEFAULT NULL,
    15. `update_by` bigint(20) DEFAULT NULL,
    16. `update_time` datetime DEFAULT NULL,
    17. `del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
    18. `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    19. PRIMARY KEY (`id`)
    20. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
    21. /*Table structure for table `role` */
    22. DROP TABLE IF EXISTS `role`;
    23. CREATE TABLE `role` (
    24. `id` bigint(20) NOT NULL AUTO_INCREMENT,
    25. `name` varchar(128) DEFAULT NULL,
    26. `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
    27. `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
    28. `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
    29. `create_by` bigint(200) DEFAULT NULL,
    30. `create_time` datetime DEFAULT NULL,
    31. `update_by` bigint(200) DEFAULT NULL,
    32. `update_time` datetime DEFAULT NULL,
    33. `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    34. PRIMARY KEY (`id`)
    35. ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
    36. /*Table structure for table `role_menu` */
    37. DROP TABLE IF EXISTS `role_menu`;
    38. CREATE TABLE `role_menu` (
    39. `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
    40. `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
    41. PRIMARY KEY (`role_id`,`menu_id`)
    42. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
    43. /*Table structure for table `user` */
    44. DROP TABLE IF EXISTS `user`;
    45. CREATE TABLE `user` (
    46. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
    47. `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
    48. `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
    49. `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
    50. `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
    51. `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
    52. `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
    53. `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
    54. `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
    55. `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
    56. `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
    57. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
    58. `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
    59. `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    60. `del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
    61. PRIMARY KEY (`id`)
    62. ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
    63. /*Table structure for table `user_role` */
    64. DROP TABLE IF EXISTS `user_role`;
    65. CREATE TABLE `user_role` (
    66. `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
    67. `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
    68. PRIMARY KEY (`user_id`,`role_id`)
    69. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    1. SELECT
    2. DISTINCT m.`perms`
    3. FROM
    4. user_role ur
    5. LEFT JOIN `role` r ON ur.`role_id` = r.`id`
    6. LEFT JOIN `role_menu` rm ON ur.`role_id` = rm.`role_id`
    7. LEFT JOIN `menu` m ON m.`id` = rm.`menu_id`
    8. WHERE
    9. user_id = 2
    10. AND r.`status` = 0
    11. AND m.`status` = 0
    1. @TableName(value="menu")
    2. @Data
    3. @AllArgsConstructor
    4. @NoArgsConstructor
    5. @JsonInclude(JsonInclude.Include.NON_NULL)
    6. public class Menu implements Serializable {
    7. private static final long serialVersionUID = -54979041104113736L;
    8. @TableId
    9. private Long id;
    10. /**
    11. * 菜单名
    12. */
    13. private String menuName;
    14. /**
    15. * 路由地址
    16. */
    17. private String path;
    18. /**
    19. * 组件路径
    20. */
    21. private String component;
    22. /**
    23. * 菜单状态(0显示 1隐藏)
    24. */
    25. private String visible;
    26. /**
    27. * 菜单状态(0正常 1停用)
    28. */
    29. private String status;
    30. /**
    31. * 权限标识
    32. */
    33. private String perms;
    34. /**
    35. * 菜单图标
    36. */
    37. private String icon;
    38. private Long createBy;
    39. private Date createTime;
    40. private Long updateBy;
    41. private Date updateTime;
    42. /**
    43. * 是否删除(0未删除 1已删除)
    44. */
    45. private Integer delFlag;
    46. /**
    47. * 备注
    48. */
    49. private String remark;
    50. }
    3.2.3.3 代码实现
    1. @Mapper
    2. public interface MenuMapper extends BaseMapper {
    3. List selectPermsByUserId(Long userId);
    4. }
    1. "1.0" encoding="UTF-8"?>
    2. mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    3. <mapper namespace="com.qhx.springsecuritydemo.mapper.MenuMapper">
    4.     <select id="selectPermsByUserId" resultType="java.lang.String">
    5.         SELECT
    6.             m.perms
    7.         FROM
    8.             `user_role` as ur
    9.                 LEFT JOIN `role` as r on ur.role_id = r.id
    10.                 LEFT JOIN `role_menu` as rm on ur.role_id = rm.role_id
    11.                 LEFT JOIN `menu` as m on rm.menu_id = m.id
    12.         WHERE
    13.             ur.`user_id` = #{userId} AND r.`status`=0 and m.`status`=0;
    14.     select>
    15. mapper>

     修改UserDetailServiceImpl。

    1. @Service
    2. public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
    3. @Autowired
    4. private MenuMapper menuMapper;
    5. @Override
    6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    7. // 用用户名查询对应User
    8. LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
    9. queryWrapper.eq(User::getUserName,username);
    10. // 判断是否空
    11. User user = this.getOne(queryWrapper);
    12. if(user == null){
    13. throw new RuntimeException("用户名或密码错误");
    14. }
    15. List authList = menuMapper.selectPermsByUserId(user.getId());
    16. return new UserDetailsImpl(user,authlist);
    17. }
    18. }

    4.异常处理

    4.1 SpringSecurity异常处理

    我们发现,就算我们登录输入的用户名和错误的:

    1.SpringSecurity没有抛出异常 2.也有没有相关响应提示信息;

    我是并不希望这样,因此要添加自定义异常处理器:

    1. /**
    2. * 授权异常处理器
    3. **/
    4. @Component
    5. public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    6. @Override
    7. public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    8. Result result = Result.error("权限不足", 403);
    9. String jsonResult = JSON.toJSONString(result);
    10. response.setStatus(200);
    11. response.setContentType("application/json");
    12. response.getWriter().write(jsonResult);
    13. }
    14. }
    1. /**
    2. * 认证异常处理器
    3. **/
    4. @Component
    5. public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    6. @Override
    7. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    8. Result result = Result.error("您认证错误/请检查你的用户名或密码是否正确", 401);
    9. String jsonResult = JSON.toJSONString(result);
    10. response.setStatus(200);
    11. response.setContentType("application/json");
    12. response.getWriter().write(jsonResult);
    13. }
    14. }

     在SecurityConfig配置中引入

    1. @Configuration
    2. @EnableGlobalMethodSecurity(prePostEnabled = true)
    3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    4. @Autowired
    5. private JwtFilter jwtFilter;
    6. @Bean
    7. public PasswordEncoder passwordEncoder(){
    8. return new BCryptPasswordEncoder();
    9. }
    10. @Autowired
    11. private AccessDeniedHandlerImpl accessDeniedHandler;
    12. @Autowired
    13. private AuthenticationEntryPoint authenticationEntryPoint;
    14. @Override
    15. protected void configure(HttpSecurity http) throws Exception {
    16. http
    17. //关闭csrf
    18. .csrf().disable()
    19. //不通过Session获取SecurityContext
    20. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    21. .and()
    22. .authorizeRequests()
    23. // 对于登录接口 允许匿名访问
    24. .antMatchers("/user/login").anonymous()
    25. // 除上面外的所有请求全部需要鉴权认证
    26. .anyRequest().authenticated();
    27. // 添加自定义token过滤器到链中
    28. http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    29. // 添加自定义异常处理器
    30. http.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
    31. .authenticationEntryPoint(authenticationEntryPoint);
    32. }
    33. @Bean
    34. @Override
    35. public AuthenticationManager authenticationManagerBean() throws Exception {
    36. return super.authenticationManagerBean();
    37. }
    38. }

    4.2 自定义异常处理

    上面除了我们SpringSecurity异常,那么还有一些其他异常,我们不希望每次捕获,并手动抛出,因此可以转化为自定义异常,用SpringBoot的全局异常处理器捕获并且抛出;

    5. 跨域

    我们现在是用postman测试,但是未来项目涉及到前后端分离项目交互的,都伴随着跨域的问题。

    后端能接受到前端原生request对象,response对象,都涉及到跨域的问题。

    SpringSecurity解决跨域

    SringMvc解决跨域

    1. @Configuration
    2. public class CorsConfig implements WebMvcConfigurer {
    3. @Override
    4. public void addCorsMappings(CorsRegistry registry) {
    5. // 设置允许跨域的路径
    6. registry.addMapping("/**")
    7. // 设置允许跨域请求的域名
    8. .allowedOriginPatterns("*")
    9. // 是否允许cookie
    10. .allowCredentials(true)
    11. // 设置允许的请求方式
    12. .allowedMethods("GET", "POST", "DELETE", "PUT")
    13. // 设置允许的header属性
    14. .allowedHeaders("*")
    15. // 跨域允许时间
    16. .maxAge(3600);
    17. }
    18. }

    自定义jwt过滤器解决跨域 

  • 相关阅读:
    tf.data.Dataset多个输入无法正确解包
    znai: 使用Markdown编写Java文档系统
    MS SQL Server 删除重复行数据
    2022牛客蔚来杯第一场
    华为再次入选2022年Gartner® SIEM魔力象限
    职场:“工作”的理解
    工欲善其事,必先利其器-使用vscode搭建go语言开发环境
    艾奇软件怎么下载安装?
    哈希函数2:用于哈希表的存储和扩容
    SpringCloud——负载均衡——Ribbon
  • 原文地址:https://blog.csdn.net/Qhx20040819/article/details/132888748