SpringSecurity 学习

学习笔记   2024-01-02 12:01   1000   0  

SpringSecurity介绍

Spring Security是基于Spring框架的权鉴框架,提供了一套WEB应用安全性的完整解决方案

SpringSecurity的两个重要核心功能就是用户认证(Authentication)和用户授权(Authorization)

RBAC模型就不多赘述了,就是将权限从用户变成了权限到角色再到用户

可以把SpringSecurity看作是Spring的过滤链FilterChain中的一环,而我们想要使用SpringSecurity就是要自定义一部分过滤器

image-20230509161632751

Security Filters

image-20230509162248617

上图显示的是Security的Filter Chain,在正式开发中我们只需要关注三个重点Filter

  • UsernamePasswordAuthenticationFilter:负责用户的认证功能

  • ExceptionTranslationFilter:负责处理过滤器链中的AccessDeniedException和AuthenticationException异常

  • FilterSecurityInterceptor:负责权限校验的处理器

SpringSecurity认证

Security流程图

上图是SpringSecirity中认证的流程,我们要使用Security就需要做下述步骤

  1. 创建一个登录接口如/user/login

    1. 调用AuthenticationManager的authenticate方法进行认证,这里的AutnenticationManager,我们可以创建一个SecurityConfig,集成WebSecurityConfigurerAdapter后,重载authenticationManagerBean()方法,可以通过AuthenticationManager调用authenticate()进行认证

    2. 这里的authenticate()需要的参数为Authentication接口的实现类,可以使用实现类 UsernamePasswordAuthenticationToken,这里的principle和credientials就是用户名和密码

    3. 认证通过后会返回一个Authentication对象,如果对象为null说明认证失败了,抛出一个异常即可

    4. 此时返回的Authentication对象中的principle变成了用户的信息

    5. 使用jwt工具类将用户信息的userid转化为token

    6. 将用户信息(包括权限信息)以userid:用户信息的kv格式保存到Redis数据库中

    7. 返回给前台由userid转换的token

    8. 前端将token保存在LocalStorage中同时每次访问都需要在Header中携带token

  2. 此外我们还需要实现UserDetailService,也就是图中的第五步

    1. 创建类实现UserDetailService,实现loadUserByUsername()方法,从数据库中获取用户信息以及权限信息

    2. 由于返回对象需要是UserDetails接口的实现类,所以我们自己实现一个LoginUser类

自己配置之后的时序图

未命名文件 (1)

在Security中的密码,我们选用官方推荐的BCryptPasswordEncoder,只需要在配置类中配置即可

这里直接贴完整的带着权限校验的配置文件

要使用注解进行权限校验的话需要注解@EnableGlobalMethodSecurity(prePostEnabled = true)

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 打开基于注解的权限控制方案
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    // 密码校验
    @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();           // 其余接口全都要权鉴
        // 将过滤器添加到UsernamePasswordAuthenticationFilter前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
        // 开启跨域
        http.cors();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }}

loginServiceImpl

@Servicepublic class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        // AuthenticationManager authenticate进行认证
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()));
        // 如果认证没通过,给出对应的提示
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败!");
        }
        // 如果认证通过了,使用userid生成一个jwt,jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userid = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userid);
        HashMap<String, String> map = new HashMap<>();
        map.put("token",jwt);
        // 把完整的用户信息存入redis,userid作为key
        redisCache.setCacheObject("login:"+userid,loginUser);
        return new ResponseResult(200,"登陆成功",map);
    }

    @Override
    public ResponseResult logout() {
        // 获取SecurityContextHolder中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        // 删除redis中的值
        redisCache.deleteObject("login:" + userid.toString());
        return new ResponseResult(200,"注销成功");
    }}

实现了UserDetails的LoginUser

@Data@NoArgsConstructorpublic class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;

    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) return authorities;
        // 把permissions中的权限信息封装成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;
    }}

实现了UserDetailsService的UserDetailsServiceImpl

@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @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("用户名不存在");
        }
        // TODO 查询对应的权限信息
        List<String> list = menuMapper.selectPermsByUserId(user.getId());
        // 把数据封装成UserDetails返回
        return new LoginUser(user,list);
    }}

SpringSecurity校验

定义JWT认证过滤器

Jwt认证过滤器放在UsernamePasswordAuthenticationFilter之前

  1. 获取token

  2. 解析token获取其中的userid

  3. 从 Redis中获取用户信息

  4. 存入SecurityContextHolder,放入SecurityContextHolder中的用户信息,在整个线程中都可以被使用

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            // 放行,会在后面的过滤器进行拦截
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        // 从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        // 用户信息存入SecurityContextHolder
        // TODO 获取权限信息封装到authenticationToken中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,
                null,
                loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request,response);
    }}

将过滤器配置在UsernamePasswordAuthenticationFilter之前

        // 将过滤器添加到UsernamePasswordAuthenticationFilter前
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

使用注解方法进行方法的权鉴@PreAuthorize内有多个方法可以查看SecurityExpressionRoot

    @GetMapping("/hello")
    @PreAuthorize("hasAuthority('sys:book:list')")
    public String hello(){
        return "hello";
    }

也可以自定义方法

@Component("ex")
public class MyExpressionRoot {
    public boolean hasAuthority(String authority){
        // 获取当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        return permissions.contains(authority);
    }}
    @GetMapping("/hello")
    @PreAuthorize("@ex.hasAuthority('sys:book:list')")
    public String hello(){
        return "hello";
    }

校验失败处理器

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,json);
    }
}

登录失败处理器

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败,请重新登录!");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,json);
    }
}

两种处理器都需要的配置文件中注册

        // 配置异常处理器
        http.exceptionHandling()
          .authenticationEntryPoint(authenticationEntryPoint)
          .accessDeniedHandler(accessDeniedHandler);

跨域

        http.cors();



博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。