SpringSecurity学习笔记

SpringSecurity学习笔记

一. 基本原理

  • 本质是一个过滤器链
  • 前后端分离的流程图如下
    SpringSecurity学习笔记_第1张图片
  • 关键是UserDetailService中方法的重写,可以将用户数据从数据库中查出
  • 在Service中手动调用ProviderManager进行校验
  • jwt的生成和在redis中的储存,校验
  • 权限管理
  • 异常处理

二. 密码加密

  • 往容器中注入BCryptPasswordEncoder,注入之后就会使用这种密码加密方式
  • 在注册的时候就可以使用encode进行加密,存的是加密后的密码
  • 有两个常用方法encode,matches
  • encode可以把原文进行加密
  • matches可以将原文和加密后的密码进行比较判断是否是相同的
  • BCryptPasswordEncoder注入进容器中就会使用
  • 这个时候,用户注册的时候存入数据库的密码要为加密后的密码,之后校验的时候就不用管了
// 在配置类中注入的加密
@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

三. 登录流程

1. 思路

  1. 不走默认的UserPasswordAuth…filter,从controller将用户参数传入
  2. 在service中手动调用校验方法
  3. 实现UserDetailService自定义校验规则,用用户名去数据库查找,返回带有权限和密码的UserLogin对象
  4. 该对象会在manager中进行密码的校验,如果校验通过,生成一个jwt作为token返回前端
  5. 校验失败被异常处理器处理,也返回前端消息
  6. 同时将详细的用户信息存入redis中,方便后面每次请求来时候的比对
    SpringSecurity学习笔记_第2张图片

2. 登录Service

  • controller就不写了,就是提供一个接口,之后调用service接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisUtils redisUtils;

    public R login(User user){
//        将用户名和密码丢给authenticationManager进行验证
        UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(upat);
//        如果验证没通过,authenticate就会是null
        if(Objects.isNull(authenticate)){
//            这里就会将错误丢到认证失败的异常丢给我们配置好的异常处理器,返回异常对应的结果给前端
            throw new RuntimeException("登陆失败");
        }
//        验证通过返回的authenticate可以获得一个UserDetails对象(这个就是自己写的实现类)
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        User auth_user = loginUser.getUser();
        String id = auth_user.getId().toString();
        String jwt = JwtUtils.createJWT(id);

//        将用户信息存入redis
        redisUtils.setCacheObject("login:"+id, loginUser);
        Map<String, String>map = new HashMap<>();
        map.put("msg", jwt);
        return R.ok(map);
    }
}

SpringSecurity学习笔记_第3张图片

3. 实现UserDetailService

  • 调用了ProviderManager的authenticate方法后,它会调用UserDetailService,而这里面的逻辑我们是想自己写的
  • 自己写可以去数据库进行查找,而不是它初始项目中用它给我们的用户名和密码
  • 自己写,实现UserDetailService接口即可
@Service
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.geology.geology_system_server.pojo.User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        com.geology.geology_system_server.pojo.User user = userMapper.selectOne(wrapper);
        if(user == null){
            throw new UsernameNotFoundException("无该用户");
        }
        System.out.println("鉴权成功");
//        从数据库中查询用户的权限信息
        List<String> permission = menuMapper.selectPermsByUserId(user.getId());
//        默认的User类也是UserDetails的实现类
//        返回查询到的用户的用户名和密码封装成User对象,这个User对象可以是自己写的,只要是实现了UserDetails就可以
        LoginUser loginUser = new LoginUser(user, permission);
        return loginUser;
    }
}

4. UserDetail对象的封装

  • 重写的这个方法的返回值是一个UserDetail对象,这个对象也自己定制一下,让该对象拥有用户名,密码,权限信息
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
    private User user;
    private List<String> permissions; // 数据库传来的权限字符串信息
    // 这个不需要存进redis中
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities = null;
    public LoginUser(User user, List<String> permission){
        this.user = user;
        this.permissions = permission;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
//        如果已经创建过权限集合,下次来就不需要再遍历数据库查来的权限字符串了
        if (authorities!=null){
            return authorities;
        }
    // 将权限信息封装到GrantedAuthority的实现类中
//        authorities = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
//            authorities.add(simpleGrantedAuthority);
//        }
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @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;
    }
}

5. 校验通过后生成token并存入redis

  • 之后的逻辑就是,如果校验成功了,就可以拿到authenticate对象,从该对象可以获得之前返回的LoginUser,就可以根据userId生成jwt,并将用户信息存入redis
  • 如果拿不到,就说明认证失败了,就抛出异常即可
    SpringSecurity学习笔记_第4张图片

四. Security的总配置类

  • 可能要整个文章看了一遍才看得懂Security配置类中一个个都是什么意思,先写在这
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //   密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Autowired
    JwtFilter jwtFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;

    // 有关放行的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/login").anonymous()  // anonymous表示未登录才能访问,如果登录则不能访问
//                .antMatchers("/user").hasAuthority("admin") //也可以在这里进行权限配置,和注解那个是一样的
                // 除此之外都要进行鉴权认证
                .anyRequest().authenticated();

//        将jwt过滤器添加到usernamepas...之前
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

//        配置认证失败和授权失败的处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

//        允许跨域
        http.cors();
    }
}

五. 验证token流程

  • 登录过后,再去访问需要登录的资源时,就要校验token

1. 思路:

  1. 查看请求头是否有token,没有的话直接放行给后面的过滤器处理
  2. 有token就用Jwt去解析,解析出来userId
  3. 拿这个userId去redis中查找,看是否能查出UserLogin对象
  4. 如果查到了对象,就将其放入SecurityContextHolder中,放行

2. JwtFilter过滤器编写

  • 因此在每次请求打来的时候,需要经过jwt校验的Filter,需要手动配置这个过滤器
@Component
public class JwtFilter extends OncePerRequestFilter {
    @Autowired
    RedisUtils redisUtils;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)){
//            如果没有token,也放行,因为后面会有别的过滤器来过滤token
            filterChain.doFilter(request, response);
            return;
        }

//        解析token
        String userId;
        try {
            Claims claims = JwtUtils.parseJWT(token);
             userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token异常");
        }

//        从redis中获取token
        String userKey = "login:" + userId;
        LoginUser loginUser = redisUtils.getCacheObject(userKey);
        if(Objects.isNull(loginUser)){
//            redis中未查到
            throw new RuntimeException("用户未登录");
        }

//        如果redis中查到了,就存入SecurityContextHolder,还有对应的权限信息
        UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(upat);
//        放行
        filterChain.doFilter(request,response);

    }
}

3. 配置类中特定位置添加过滤器

  • 这里是否在redis中查到都放行了,但是如果查到了,会将LoginUser对象和对应的权限信息放进SecurityContextHolder中,如果没放的话,后续自有filter会处理认证失败的情况

  • 之后要在Security配置类中将JwtFilter放在UsernamePasswordAuthenticationFilter之前,这样每次来就会先经过这个filter
    SpringSecurity学习笔记_第5张图片

六. 登出设置

思路:

  • 由于登出的前提是之前已经登陆了,因此访问的时候也经过了jwtFilter,因此用户信息还存在SecurityContextHolder
  • 将redis中的用户对象删除即可,因为下一次经过jwtFilter的时候从redis中取不到对象,就不会把这个用户放入SecurityContextHolder了
    public R logout(){
//        从ContextHolder中获取授权对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        boolean suc = redisUtils.deleteObject("login:" + loginUser.getUser().getId());
        if(suc){
            R.ok("登出成功");
        }
        return R.error("登录失败");
    }

七. 权限设置

  • 权限设置在只在前端是不够的,前端防君子,后端防小人
  • 在FilterSecurityInterceptor中会从SecurityContextHolder中获取权限信息,判断用户是否拥有当前资源的访问权限

1. 开启权限注解

  • 在security的配置类上添加注解@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

2. 指定资源需要什么权限

  • 在controller中可以指定某些路径下的资源需要特定权限才可以访问
  @PreAuthorize("hasAnyAuthority('data:write', 'data:read')") // 有这些权限中的任意一个就可以访问
//    @PreAuthorize("hasAuthority('role')") // 有这个权限才可以访问
//    @PreAuthorize("hasRole('admin')") // 会 将ROLE_ 这个拼接到用户权限之前,最终看有没ROLE_admin这个权限
//   @PreAuthorize("@myExpression.hasAuthority('data:write')")  // 通过获得到注入了的bean中的方法(自定义的校验方法)来判断
@GetMapping("/hello")
........

3. 将UserLogin对象进行权限改装

  • 在UserLogin中需要加入权限相关配置
  • 关键是在重写的getAuthorities中需要将权限字符串依次变为GrantedAuthority,封装进一个集合中返回
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
    private User user;
    private List<String> permissions; // 数据库传来的权限字符串信息
    // 这个不需要存进redis中
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities = null;
    public LoginUser(User user, List<String> permission){
        this.user = user;
        this.permissions = permission;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
//        如果已经创建过权限集合,下次来就不需要再遍历数据库查来的权限字符串了
        if (authorities!=null){
            return authorities;
        }
    // 将权限信息封装到GrantedAuthority的实现类中
//        authorities = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
//            authorities.add(simpleGrantedAuthority);
//        }
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @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;
    }
}

4. 将权限信息放进SecurityContextHolder

  • JwtFilter中如果查到token了说明该用户登陆过,同时把权限信息放入到上下文中
    SpringSecurity学习笔记_第6张图片

八. 对授权失败和认证失败的处理器

  • 如果登录认证失败或授权失败,都抛出了异常,可以通过实现两个类分别处理授权失败和认证失败

1. 认证失败

  • 实现AccessDeniedHandler
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//        授权失败的处理器
        R res = R.error(HttpStatus.FORBIDDEN.value(), "您的权限不够");
        WebUtils.renderString(response, JSON.toJSONString(res));
    }
}

2. 授权失败

  • 实现AuthenticationEntryPoint
@Component
//异常处理
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//        认证失败的处理器
        R res = R.error(HttpStatus.UNAUTHORIZED.value(), "认证失败");
        WebUtils.renderString(response, JSON.toJSONString(res));
    }
}

3. 在security配置类中配置

SpringSecurity学习笔记_第7张图片

你可能感兴趣的:(java,springboot)