前言
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
此文章在有一定的SpringSecurity基础的看,包括Springboot+SpringSecurity+JWT+Redis进行整合。
记得把之前拦截器注释掉,它已经是过去时了。说实话刚开始学确实摸不懂头脑,但是学完自己写出来就感到无比的喜悦,这可能就是自己选择这么方向的原因吧。加油兄弟们。
项目更改地方
SpringSecurity
-
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
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(); } }
-
增加一张表role和user和role多对多表
role 主键 id 和一个 varchar 的name ,必须加ROLE_
user_role 主键id和两个外键分别对应user和role表
-
实体类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; } }
-
自定义登录逻辑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; } }
-
登录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
进行判定。看到这是不是脑袋里有思路了,我建议多打几个断点
去跟这断点走这样理解更深。 -
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); } }
测试
- 测试登录
- 每次都会经过这个自定义登录方法
- 和之前jwt一样获取资源还是header加token
- 权限不足,如果要用
Authorization
记得在配置文件改一下token.header=Authorization
总结
通过代码可以发现我代码里有很多的System.out.println("-**************login");
虽然已经删除了很多了。这是我在代码出错或者要看到那个方法执行以及方法中某个对象或者变量值都是用这个方法进行判断,不知道好不好但是多起来就难受了
我看看到很多都是用log所以下一步整合日志框架。
其实并不难,就看自己下不下劲了。我看连写将近一周时间把他搞完,至少看了三个完整视频,五篇以上相关文章,而且还没有看源码,中间出现了很多错误不知道为什么很多网上都没有相关问题。重点还是在于登录逻辑,这只是框架的第二种密码登录模式还是比较简单的。