Home
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • JavaSE
  • JVM
  • JUC
  • Netty
  • CPP
  • QT
  • UE
  • Go
  • Gin
  • Gorm
  • HTML
  • CSS
  • JavaScript
  • vue2
  • TypeScript
  • vue3
  • react
  • Spring
  • SpringMVC
  • Mybatis
  • SpringBoot
  • SpringSecurity
  • SpringCloud
  • Mysql
  • Redis
  • 消息中间件
  • RPC
  • 分布式锁
  • 分布式事务
  • 个人博客
  • 弹幕视频平台
  • API网关
  • 售票系统
  • 消息推送平台
  • SaaS短链接系统
  • Linux
  • Docker
  • Git
GitHub (opens new window)
Home
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • JavaSE
  • JVM
  • JUC
  • Netty
  • CPP
  • QT
  • UE
  • Go
  • Gin
  • Gorm
  • HTML
  • CSS
  • JavaScript
  • vue2
  • TypeScript
  • vue3
  • react
  • Spring
  • SpringMVC
  • Mybatis
  • SpringBoot
  • SpringSecurity
  • SpringCloud
  • Mysql
  • Redis
  • 消息中间件
  • RPC
  • 分布式锁
  • 分布式事务
  • 个人博客
  • 弹幕视频平台
  • API网关
  • 售票系统
  • 消息推送平台
  • SaaS短链接系统
  • Linux
  • Docker
  • Git
GitHub (opens new window)
  • 认证
    • 登录校验流程
    • 过滤器链
    • 认证流程
    • 代码实现
      • 思路
      • 添加依赖
      • 数据库校验用户
      • 密码加密存储
      • 登录接口
      • 退出登录
      • 测试
  • 授权
  • SpringSecurity
Nreal
2023-12-19
目录

认证

# 登录校验流程

# 过滤器链

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器:

UsernamePasswordAuthenticationFilter:负责处理在登陆页面填写了用户名密码后的登陆请求;

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

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

# 认证流程

Authentication接口: 封装用户相关信息;

**AuthenticationManager接口:**其实现类调用DaoAuthenticationProvider的authenticate方法进行认证;

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

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

# 代码实现

# 思路

登录:

  1. 自定义登录接口:调用ProviderManager的authenticate方法进行认证,如果认证通过生成jwt把用户信息存入redis中;
  2. 自定义UserDetailService:其实现类去数据库查询用户信息;

校验:

​ 定义JWT认证过滤器:

  1. 获取token;
  2. 解析token获取其中userId;
  3. 从redis获取用户信息;
  4. 存入SecurityContextHolder;

# 添加依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!--SpringSecurity启动器-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.7.15</version>
    </dependency>

    <!--redis依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--common-pool-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    <!--fastjson依赖-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.33</version>
    </dependency>
    <!--jwt依赖-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>


    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>
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

# 数据库校验用户

实现UserDetailsService接口,从数据库查询用户:

@Service
public class UserDetailsServiceImpl 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 user = userMapper.selectOne(queryWrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名密码错误");
        }
        return new LoginUser(user);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

LoginUser包装UserDetails(后续还能封装权限):

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return 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

# 密码加密存储

配置类SecurityConfig中加入Bean:

@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
1
2
3
4

测试:

@Test
public void TestBCryptPasswordEncoder(){
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    System.out.println(bCryptPasswordEncoder.encode("123"));
    System.out.println(bCryptPasswordEncoder.matches("123","$2a$10$C0V/bLHc7cmtuE11kWtkputF56PNr3miVAz5JsTT/exjLkPZkF9Ei"));
}
1
2
3
4
5
6

将数据库中密码修改为加密后的;

# 登录接口

自定义登录接口,让SpringSecurity对这个接口放行,用户不登陆也能访问;

接口中通过AuthenticationManager的authenticate方法进行用户验证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                //其它接口需要鉴权认证
                .anyRequest().authenticated();

        //把token校验过滤器添加到过滤器链中
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
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

认证成功后生成jwt,为了让用户下回请求时能通过jwt识别出具体的是哪个用户,可以把用户信息存入redis,可以把用户id作为key;

LoginServiceImpl

@Override
public ResponseResult login(User user) {
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
    Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    if(Objects.isNull(authenticate)) {
        throw new RuntimeException("用户名或密码错误");
    }
    LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
    String userId = loginUser.getUser().getId().toString();
    HashMap<String,Object> tokenMap = new HashMap<>();
    tokenMap.put("userId",userId);
    String jwt = JwtUtil.createToken(tokenMap,HOUR,24);
    redisTemplate.opsForValue().set("login:"+userId, JSON.toJSONString(loginUser));
    HashMap<String, String> map = new HashMap<>();
    map.put("token",jwt);
    return new ResponseResult(200,"登录成功",map);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

到这仅将jwt返回给前端,前端需要根据请求头中的token,进行解析出userId,去redis获取对应LoginUser对象,封装为Authentication对象存入SecurityContextHolder;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)) {
            filterChain.doFilter(request, response);
            return ;
        }
        //解析token
        String userId;
        try {
             userId = (String) JwtUtil.parseToken(token, "userId");
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userId;
        String jsonStr = (String) redisTemplate.opsForValue().get(redisKey);
        LoginUser loginUser = JSON.parseObject(jsonStr,LoginUser.class);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }

        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        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

此过滤器链需要在其它过滤器之前,在SecurityConfig中的addFilterBefore中配置;

# 退出登录

获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可;

@Override
public ResponseResult logout() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    Long userId = loginUser.getUser().getId();
    redisTemplate.delete("login:"+userId);
    return new ResponseResult(200,"退出成功");
}
1
2
3
4
5
6
7
8

# 测试

登录测试:

登出测试:

#

授权

授权→

Theme by Vdoing | Copyright © 2021-2024
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式