参考文章:
https://www.bilibili.com/video/BV15a411A7kP?p=1
https://www.bilibili.com/video/BV1d7411y7Uu
1. 简介
SpringSecurity在B站上讲的真的很糟糕,特别是与JWT的整合,看完视频后,依照老师的思路,自己整合了一下,现在讲自己的成果与大家分享,无需使用SpringCloud,只用了SpringBoot即可!在这里也推荐一下第二个b站的视频,他讲的SpringSecurity会稍微好一点,对代码也有会说明。
2. 类的说明
在开始之间先说明一下需要用到几个SpringSecurity的类
2.1 UsernamePasswordAuthenticationFilter
拦截登录请求并获取用户输入的账号和密码,然后把账号密码封装到 UsernamePasswordAuthenticationToken
(未受信任的认证凭据)中,然后将 “认证凭据" 交给 我们配置 的AuthenticationManager
去作认证
过程如下:
理解了上述流程后,我们可以做以下事情:
- 定制我们的登录请求URI和请求方式。
- 登录请求参数的格式定制化。
- 拦截登录。
- 登录成功后调用的方法。
- 登录失败后调用的方法。
详见:https://felord.cn/usernamePasswordAuthenticationFilter.html
2.2 AuthenticationManager
认证管理器。
WebSecurityConfigurerAdapter
(配置SpringSecurity的类)中的void configure(AuthenticationManagerBuilder auth)
是配置AuthenticationManager
的地方,里面只有一个Authentication authenticate(Authentication authentication)
方法,他的作用是对 未受信任的认证凭据 做认证,认证成功则返回 授信状态的认证凭据 ,否则将抛出认证异常AuthenticationException
详见:https://www.jianshu.com/p/56e53d4ec1a9
2.3 Authentication
认证对象,用于保存用户的信息。认证凭据
UsernamePasswordAuthenticationToken
实现了该接口。属性如下:Boolean authenticated
:确定当前用户是否受信,为true
时受信,为false
时不受信。上述的返回 授信状态的认证凭据,就是将该值设置为true
。
Object principal
:主体。在未受信的情况是,这是用户名;在已受信的情况下,这是UserDetails
的实现类。
Object credentials
:在未受信的情况是,这是密码;在已受信的情况下,这是null
,所以这里也可以设置JWT
。
Collection authorities
:主体的权限集合。由AuthenticationManager
设置的,所以刚登录的时候,这里没有值;登录成功后主体中的权限会设置在这里。
Object details
:存储关于身份验证请求的其他详细信息。这些可能是IP地址、证书编号等。未使用返回null
详见:https://felord.cn/securityContext.html
2.4 SecurityContextHolder
这是一个工具类,用于设置、获取、清理
SecurityContext
,这个SecurityContext
是存储Authentication
的容器,也就是说可以将受信用户存放到SecurityContext
中,允许该请求的后续访问。
详见:https://felord.cn/spring-security-securitycontext.html
2.5 BasicAuthenticationFilter
负责处理
HTTP
头中显示的基本身份验证凭据。
重写void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
即可从HTTP
头获取JWT
详见:https://felord.cn/spring-security-filters.html#3-24-BasicAuthenticationFilter
3. 思路
登录:
退出登录:
JWT认证:
4. 核心代码说明
SecurityConfig.class
配置类@Configuration public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TokenManager tokenManager; @Autowired private RedisTemplate redisTemplate; @Autowired private UserDetailsService userDetailsService; @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .exceptionHandling() .authenticationEntryPoint(new UnauthEnrtyPoint()) //无权限访问时,做出的处理 .and().authorizeRequests() .antMatchers("/test/guest").permitAll() //所有请求都可以访问 .antMatchers("/test/user").hasRole("user") //只有角色为 user 的可以访问 .antMatchers("/test/admin").hasRole("admin") //只有角色为 admin 的可以访问 .anyRequest().authenticated() .and().logout() .logoutUrl("/test/logout") //设置退出路径 .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)) //退出时所做的逻辑 .and().addFilter(new TokenLoginFilter(tokenManager,redisTemplate,authenticationManager())) //登录时的过滤器 .addFilter(new TokenAuthFilter(tokenManager,redisTemplate,authenticationManager())) //验证JWT的过滤器 .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } }
TokenLoginFilter.class
登录认证public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; private AuthenticationManager authenticationManager; public TokenLoginFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) { this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; this.authenticationManager = authenticationManager; //定制我们的登录请求URI和请求方式。 this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/test/login", "POST")); } /** * 拦截登录。获取表单的用户名与密码 */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { //使用 请求体 传递登录参数,更加安全 LoginData user = new ObjectMapper().readValue(request.getInputStream(), LoginData.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(); } } /** * 登录成功后调用的方法 * 返回token */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityUser user = (SecurityUser) authResult.getPrincipal(); //根据用户名生成token String token = tokenManager.createToken(user.getUsername()); response.setHeader("token", token); //将 用户名:角色 放入redis中 redisTemplate.opsForValue().set(user.getUsername(), user.getRole().getRole_name()); } /** * 登录失败后调用的方法 */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.getWriter().print("login fail"); } }
TokenAuthFilter.class
授权过滤,校验token的有效性public class TokenAuthFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) { super(authenticationManager); this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } /** * 对HTTP请求头做处理 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //授权 UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); //授权失败 if (authRequest == null) { chain.doFilter(request, response); return; } //如果有授权,放到权限上下文(容器)中 SecurityContextHolder.getContext().setAuthentication(authRequest); chain.doFilter(request, response); } /** * 认证token是否合法,若合法,返回认证,否则返回null */ private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { //获取token String token = request.getHeader("token"); if (!StringUtils.isEmpty(token)) { //从token中获取username String username = tokenManager.getUserInfoFromToken(token); //redis中,根据username获取角色 String role_name = (String) redisTemplate.opsForValue().get(username); Collection<GrantedAuthority> authorities = new ArrayList<>(); List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(role_name); authorities.addAll(list); if (!StringUtils.isEmpty(username)) { return new UsernamePasswordAuthenticationToken(username, token, authorities); } } return null; } }
5. 源码