自己写的springboot第一个博客项目(四)SpringSecurity+JWT+Redis

436 阅读6分钟

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

此文章在有一定的SpringSecurity基础的看,包括Springboot+SpringSecurity+JWT+Redis进行整合。

记得把之前拦截器注释掉,它已经是过去时了。说实话刚开始学确实摸不懂头脑,但是学完自己写出来就感到无比的喜悦,这可能就是自己选择这么方向的原因吧。加油兄弟们。

项目更改地方

SpringSecurity
  1. pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
     </dependency>
    
  2. SecurityConfig

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private DataSource datasource;
    
        @Autowired
        private JwtAuthTokenFilter jwtAuthTokenFilter;
    
        //授权
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            http.authorizeRequests()
                    .antMatchers(HttpMethod.POST,"/login").permitAll()
                    .antMatchers(
                            HttpMethod.GET,
                            "/*.html"
                    ).permitAll()
                    .antMatchers("/toLogin").permitAll()
                    .antMatchers("/blog/blogs").hasRole("admin")
                    .antMatchers("/blog/{id}").hasRole("vip1")
                    .anyRequest().authenticated();
    //                .antMatchers("/blogs").hasIpAddress("0:0:0:0:0:0:0:1");
    
          http.formLogin()
                  //自定义登录页面
                 .loginProcessingUrl("/login")
                  .loginPage("/toLogin")
                  .successForwardUrl("/blogs")
              //自定义跳转
    //              .successHandler(new MyAuthenticationSuccessHandler("/"))
                 .failureForwardUrl("/toError");
    //              .failureHandler(new MyAuthenticationFailureHandler("/error"));
            //登出
          http.logout()
                  .logoutSuccessUrl("/toLogin");
            //禁用跨站csrf攻击防御,否则无法登陆成功
          http.csrf().disable();
          //登出功能
            httpSecurity.logout().logoutUrl("/logout");
    //        http.rememberMe()
    //                //失效时间
    //                .tokenValiditySeconds(60)
    //                //自定义登录逻辑
    //                .userDetailsService(userDetailsService)
    //                //持久层对象
    //                .tokenRepository(persistentTokenRepository);
    
            //  添加JWT  filter, 在每次http请求前进行拦截
            http.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);
    
            //异常处理
            http.exceptionHandling()
                    .accessDeniedHandler(new MyAccessDeniedHandler());
        }
    
    //    @Bean
    //    public PersistentTokenRepository getPersistentTokenRepository(){
    //        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    //        jdbcTokenRepository.setDataSource(datasource);
    //        //自动建表,第一次启动需要,第二次需要注释
    ////        jdbcTokenRepository.setCreateTableOnStartup(true);
    //        return  jdbcTokenRepository;
    //    }
    
        //认证
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            //调用DetailsService完成用户身份验证              设置密码加密方式
            auth.userDetailsService(userDetailsService).passwordEncoder(getBCryptPasswordEncoder());
        }
        // 在通过数据库验证登录的方式中不需要配置此种密码加密方式, 因为已经在JWT配置中指定
        @Bean
        public BCryptPasswordEncoder getBCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
        //将AuthenticationManager注入spring中,要不然可调用不到
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
    }
    
  3. 增加一张表role和user和role多对多表

    role 主键 id 和一个 varchar 的name ,必须加ROLE_

    role.jpg

    user_role 主键id和两个外键分别对应user和role表

    userrole.jpg

  4. 实体类user修改

    继承Security的UserDetails接口。如果不用的话会报错,框架就不认咱们的实体类还是默认框架自己的实体类。实现接口中的方法注意点默认的四个false改为true

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @TableName("m_user")
    public class User implements UserDetails {
    
        public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
            this.username = username;
            this.password = password;
            this.authorities = authorities;
        }
    
        @TableId(value = "id", type = IdType.AUTO)
        private Long id;
        @NotBlank(message = "名字不能为空")
        private String username;
        @NotBlank(message = "头像不能为空")
        private String avatar;
        @Email
        private String email;
        @NotBlank(message = "密码不能为空")
        private String password;
    
        private Integer status;
        @JsonFormat(pattern="yyyy-MM-dd")
        private Date created;
        @JsonFormat(pattern="yyyy-MM-dd")
        private Date lastLogin;
        //增加权限字段
        private Collection<? extends GrantedAuthority>   authorities;
    
        private static final long serialVersionUID = 1L;
        //这里默认为null 
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
    
        @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. 自定义登录逻辑UserDetailsServiceImpl继承UserDetailsService接口

    这里逻辑非常简单,就是从数据库查数据。通过username判断用户(password在框架中封装,由于我的数据库的数据没有进行加密,所以我查出来数据进行了加密),对象为空就可以抛出UsernameNotFoundException异常,这是肯定要有的。

    查询权限表SELECT * from m_role where id in (select roleid FROM m_userrole where userid = #{userid})查到名下所有权限,拿出路径用,号分割拼成字符串,然后用AuthorityUtils.commaSeparatedStringToAuthorityList()进行分割存入user对象中。把当前user存入线程以便后续用到,返回user对象此处user必须继承过UserDetails的对象。

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserService userService;
    
        @Autowired
        private RoleService roleService;
    
        @Autowired
        private RedisUtil reidsUtil;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            System.out.println("**********UserDetailsServiceImpl********");
            User user = userService.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("登录用户:" + username + "不存在");
            }
            //查询用户所有权限
                List<Role> roles = roleService.getAllByUserId(user.getId());
                String authorities = "";
                int index = 0;
                //拿出来所有权限名字 用,分割
                for (Role role : roles) {
                    index++;
                    authorities += role.getName();
                    authorities += ",";
                }
                System.out.println("roles-------" + authorities);
                //将数据库的roles解析为UserDetails的权限集
                //AuthorityUtils.commaSeparatedStringToAuthorityList将逗号分隔的字符集转成权限对象列表
         user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
                //将当前用户存到user线程
                UserTheadLocal.put(user);
                System.out.println(user);
                System.out.println(user.getAuthorities());
                return user;
        }
    }
    
  6. 登录loginController

    只是一个简单的api调用方法即可没有多余判断语句。重点在于之前我这里有注解@RequestBody,不知道为什么我加入了security之后总是报错null,有懂得同学谢谢解答一下。

    @RestController
    public class LoginController {
    
        @Autowired
        private UserService userService;
        
    
        @ApiOperation(value = "登录")
        @PostMapping("/login")
        public Result login(LoginDto loginDto){
            System.out.println("login+++++++++++++++++");
            System.out.println(loginDto.getUsername()+"******"+loginDto.getPassword());
            String token = userService.login(loginDto.getUsername(), loginDto.getPassword());
            return Result.success(token);
        }
    
    }
    

    UserService中login实现 。首先要问自己如果不用框架应该怎么写呢?

           @Override
        public String login(String username, String password){
            System.out.println("-**************login");
            //用户验证
            Authentication authentication = null;
            try {
                // 进行身份验证,
                authentication = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(username, password));
            } catch (Exception e) {
                throw new RuntimeException("用户名密码错误");
            }
    
            User user = (User) authentication.getPrincipal();
            // 生成token
            String token = JwtUtils.createToken(user);
            System.out.println("token+++++++"+token);
            redisUtil.set("token"+token,user.getUsername(),600);
            System.out.println("loca++++"+ UserTheadLocal.get());
            return token;
        }
    

    我们会直接从数据库里去查找此人是否存在就可以判定。而现在我们需要去实现框架验证登录逻辑获取user对象UsernamePasswordAuthenticationToken通过用户生成authentication,然后authenticationManager.authenticate()对特定的authentication进行认证功能,认证成功后的Authentication就变成授信凭据,并触发认证成功的事件。认证失败的就抛出异常触发认证失败的事件。其中authenticationManager还会调用我们自定义的UserDetailsServiceImpl进行判定。看到这是不是脑袋里有思路了,我建议多打几个断点去跟这断点走这样理解更深。

  7. Filter

    UsernamePasswordAuthenticationToken判断用户是否经过认证,SecurityContextHolder.getContext().setAuthentication()授权用户,将返回Authentication 对象赋予给当前的 SecurityContext。

    @Component
    public class JwtAuthTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        private UserDetailsServiceImpl userDetailsService;
    
        @Autowired
        private RedisUtil redisUtil;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            System.out.println("*********filter*******");
            String requestURI = request.getRequestURI();
            System.out.println("*******-----"+requestURI+"*****-++++++");
            String token = request.getHeader("Authorization");
            System.out.println(token);
    
                if (!StringUtils.isEmpty(token)) {
                    String  username = (String) redisUtil.get("token" + token);
                    User user = UserTheadLocal.get();
                    if (username != user.getUsername() && SecurityContextHolder.getContext().getAuthentication() == null) {
                        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                         //交给security;管理,在之后过滤器就不会拦截进行二次授权
                            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                           
                           //设置用户身份授权 
          SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
    
            filterChain.doFilter(request, response);
        }
    }
    
测试
  • 测试登录 security+login.jpg
  • 每次都会经过这个自定义登录方法 每次都调用userImpl.jpg
  • 和之前jwt一样获取资源还是header加token jwtblog1success.jpg
  • 权限不足,如果要用Authorization记得在配置文件改一下token.header=Authorization

权限不足.jpg

总结

通过代码可以发现我代码里有很多的System.out.println("-**************login");虽然已经删除了很多了。这是我在代码出错或者要看到那个方法执行以及方法中某个对象或者变量值都是用这个方法进行判断,不知道好不好但是多起来就难受了 运行.jpg 我看看到很多都是用log所以下一步整合日志框架。

其实并不难,就看自己下不下劲了。我看连写将近一周时间把他搞完,至少看了三个完整视频,五篇以上相关文章,而且还没有看源码,中间出现了很多错误不知道为什么很多网上都没有相关问题。重点还是在于登录逻辑,这只是框架的第二种密码登录模式还是比较简单的。

欢迎大家指导批评