SpringSecurity注解鉴权(整合springboot,jwt,redis)

1、用户实体类

该类实现了UserDetails接口

public class SysUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "用户编号")
    @TableId(value = "user_id", type = IdType.ID_WORKER_STR)
    private String userId;

    @ApiModelProperty(value = "用户名")
    private String userName;

    @ApiModelProperty(value = "用户手机号码 ")
    private String userPhone;

    @ApiModelProperty(value = "用户密码")
    private String userPassword;

    @ApiModelProperty(value = "用户最近一次登录时间")
    private String userLastLoginTime;

    @ApiModelProperty(value = "用户注册时间")
    private String userCreateTime;

    @ApiModelProperty(value = "用户状态,0正常,-1删除")
    private Integer userStatus;

    @ApiModelProperty(value = "用户的角色列表")
    private List roleCodes;

    //重写UserDetails中的方法,得到权限列表,权限列表中存储的是角色名
    @Override
    public Collection getAuthorities() {
        Collection authorities = new ArrayList<>();
        //将用户的角色以SimpleGrantedAuthority的形式存入authorities中
        for(String roleCode : roleCodes) {
            if(StringUtils.isEmpty(roleCode)) continue;
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(roleCode);
            authorities.add(authority);
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return userPassword;
    }

    @Override
    public String getUsername() {
        return userName;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
2、登录过滤器

内含登录成功或失败后的处理方法

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    /**
     * 该redisTemplate不能直接由容器注入
     *       因为TokenLoginFilter类不在spring容器内,所以redisTemplate不能直接注入
     *       该redisTemplate是通过MyWebSecurityConfig中TokenLoginFilter的构造条件注入的。
     *       也就是下面 public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate)这个方法
     */

    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        //从MyWebSecurityConfig得到redisTemplate
        this.redisTemplate = redisTemplate;
        this.setPostOnly(false);
        //自定义登录url
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login","POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)  {
        try {
            //从请求中读取数据
            LoginForm user = new ObjectMapper().readValue(req.getInputStream(), LoginForm.class);
            //两种抛异常方法
            //自定义AccountException
            if (user.getUserphone() == null || user.getUserphone().equals("")){
                throw new AccountException("账号为空");
            }
            //security中AuthenticationException的自雷异常
            if (user.getPassword() == null || user.getPassword().equals("")){
                throw new BadCredentialsException("密码为空");
            }
            //调方法
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUserphone(), user.getPassword());
            return authenticationManager.authenticate(token);
        } catch (IOException e) {
            ResponseUtil.out(res, Result.code(ResultCode.fail).message("数据读取错误"));
        }
        return null;
    }

    /**
     * 登录成功
     *      成功后创建token返回前端,并将用户权限存入redis
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        SysUser user = (SysUser) auth.getPrincipal();
        String userPhone = user.getUserPhone();
        String userName = user.getUsername();
        String jwtToken = JwtUtils.getJwtToken(userPhone, userName);
        redisTemplate.opsForValue().set(userPhone,user.getRoleCodes());
        redisTemplate.opsForValue().set("token",jwtToken);
        ResponseUtil.out(response, Result.code(ResultCode.SUCCESS).message("登录成功!").data("token", jwtToken));

    }

    /**
     * 登录失败
     *      失败后提示用户登录失败
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, Result.code(ResultCode.fail).message(e.getMessage()));
    }
}
2.1、LoginForm类

用一个简单的类来接受前端出来的登录信息

public class LoginForm {
    private String userphone;
    private String password;
}
3、MyUserDetailsService类

实现UserDetailsService接口,重写loadUserByUsername方法,按自己的实际需求来编写验证规则

public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private SysUserService userService;

    /***
     * 根据账号获取用户信息
     */
    @Override
    public UserDetails loadUserByUsername(String userphone) throws UsernameNotFoundException {
        //调用userService得到SysUser
        SysUser user = userService.findByPhone(userphone);
        if (user == null){
            throw new AccountException("账号或密码错误");
        }
        //SysUser继承了UserDetails接口,可直接返回
        return user;
    }
}
4、TokenAuthenticationFilter类

该类为token校验器,并封装了用户权限,保存至security上下文中

public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    private RedisTemplate redisTemplate;

    //之所以redisTemplate能生效,是因为该RedisTemplate是在MyWebSecurityConfig传入的
    public TokenAuthenticationFilter(AuthenticationManager authManager, RedisTemplate redisTemplate) {
        super(authManager);
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String token = request.getHeader("token");
        String jwtToken = (String) redisTemplate.opsForValue().get("token");
        if (token == null || !token.equals(jwtToken) ){
            //token为空时,直接放行到下一条过滤器(此时SecurityContext中没有任何权限,放行后会被最终的过滤器检测到无权限,然后禁止访问)
            chain.doFilter(request, response);
            System.out.println("当token为空或格式错误时 直接放行");
            return;
        }
        //根据token获得authenticationToken
        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(token);
        //将authenticationToken存入SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        chain.doFilter(request, response);
    }
    /**
     * 这里从token中获取用户信息并新建一个UsernamePasswordAuthenticationToken
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String token) {

        Claims claims = JwtUtils.getClaims(token);
        String userPhone = (String) claims.get("userPhone");
        String userName = (String) claims.get("userName");
        if (userPhone != null && userName != null) {
            /**
             * 1、从redis中取出用户拥有的角色
             * 2、将其转化为SimpleGrantedAuthority
             * 3、封装至UsernamePasswordAuthenticationToken,方便后面鉴权时取出
             *   UsernamePasswordAuthenticationToken是接口Authentication的一个实现类
             */
            List roleCodes = (List)redisTemplate.opsForValue().get(userPhone);
            Collection authorities = new ArrayList<>();
            for(String roleCode : roleCodes) {
                if(StringUtils.isEmpty(roleCode)) continue;
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(roleCode);
                authorities.add(authority);
            }
            //todo
            System.out.println(authorities+"==============");
            return new UsernamePasswordAuthenticationToken(userPhone, userName, authorities);
        }
        return null;
    }
}
5、统一异常处理
//该类处理认证异常
public class MyAuthorizedEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        ResponseUtil.out(response, Result.code(ResultCode.LOGIN_ERR).message("请登录后再进行操作!"));
    }
}
//该类处理鉴权异常
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        ResponseUtil.out(httpServletResponse, Result.code(ResultCode.ACCESS_NOT).message("您的权限不足!"));
    }
}
6、注销处理
public class MyLogoutHandler implements LogoutHandler {

    private RedisTemplate redisTemplate;

    public MyLogoutHandler(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String token = request.getHeader("token");
        String userPhone = JwtUtils.getUserPhoneByJwtToken(request);
        if (token != null){
            //清除缓存里的信息
            redisTemplate.delete(userPhone);
            redisTemplate.delete("token");
        }
        ResponseUtil.out(response, Result.code(ResultCode.SUCCESS).message("退出成功"));

    }
}
6、security配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解模式!!
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    //未授权
    @Autowired
    private MyAuthorizedEntryPoint myAuthorizedEntryPoint;

    //访问拒绝
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    //在WebSecurityConfig中注入,为了后面传入其他的组件
    @Autowired
    private RedisTemplate redisTemplate;


    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 设置默认的加密方式(强hash方式加密)
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置认证方式等,数据库中存储的是加密后的密码
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //http相关的配置,包括登入登出、异常处理、会话管理等
        http.cors().and().csrf().disable();//关闭了csrf拦截的过滤器
        http.authorizeRequests().
//                antMatchers("/getUser").hasAuthority("query_user").
                //所有请求都需要被认证
                anyRequest().authenticated().
                and().formLogin().usernameParameter("userphone").permitAll().//允许所有用户
                and().logout().logoutUrl("/user/logout").addLogoutHandler(new MyLogoutHandler(redisTemplate)).
                //未认证和未授权时的处理
                and().exceptionHandling().authenticationEntryPoint(myAuthorizedEntryPoint).accessDeniedHandler(myAccessDeniedHandler).
                and()
                //关闭session  用token验证,所以关闭session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
                and()
                    //不能用自动装配方式,因为authenticationManager不能自动装配
                    //登录过滤器,同时成功后创建token,该过滤器因为没有注入到spring容器中,所以创建一个构造方法,在配置中将redisTemplate传入该过滤器中
                    .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate))
                   //Token,同时成功后创建token,该过滤器因为没有注入到spring容器中,所以创建一个构造方法,在配置中将redisTemplate传入该过滤器中
                    .addFilter(new TokenAuthenticationFilter(authenticationManager(), redisTemplate)).httpBasic();
    }
}
7、postman测试

首先SysUserController中有三个测试接口,第一个接口认证后即可访问,第二个接口需要登录的用户拥有ROLE_ADMIN角色,第三个接口需要用户拥有ROLE_USER角色。

    @GetMapping("/lande")
    public String lande(){
        return "hello,lande";
    }

    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    @GetMapping("/test")
    public String test1(){
        return "ceshihahaha";
    }

    @PreAuthorize("hasAnyRole('ROLE_USER')")
    @GetMapping("/hello")
    public String hello(){
        return "hellohahaha";
    }
登录:该用户仅拥有ROLE_USER角色

返回了token信息

测试第一个接口:

请求头中带上token,因为security配置类中关闭了session,后续请求必须带上token才能访问。

访问成功。

测试第二个接口

该接口需要ROLE_ADMIN,我们已登录的用户只拥有ROLE_USER,所以该接口不能访问。

结果符合预期

测试第三个接口

该接口需要ROLE_USER,已登录用户可以访问

结果符合预期

如果请求头中不带token或token错误

项目源码地址:https://github.com/lan-de/SpringSecurity-01

你可能感兴趣的:(SpringSecurity注解鉴权(整合springboot,jwt,redis))