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<String> roleCodes;

    //重写UserDetails中的方法,得到权限列表,权限列表中存储的是角色名
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> 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<String> roleCodes = (List<String>)redisTemplate.opsForValue().get(userPhone);
            Collection<GrantedAuthority> 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335