admin管理员组

文章数量:1794759

SpringSecurity

SpringSecurity

一、SpringSecurity

前面讲解了SpringSecurity的动态认证和动态权限角色,我们都知道在现在大多都是微服务前后端分离的模式开发,前面讲的还是基于Session的,本篇我们整合JWT实现使用Token认证授权。

上篇文章地址:

在开始前需要了解JWT,如果不了解,可以先看下下面这篇我的博客:

二、SpringSecurity 整合JWT使用 Token 认证授权

本篇文章还是接着上篇文章进行讲解,数据库还是使用上两篇文章中创建的数据库:

由于我们要使用JWT生成Token和存储一些信息,所以先引入JWT的依赖:

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.6.0</version>
</dependency>

编写JwtTool工具:

@Data
@Component
public class JwtTool {private String key = "com.bxc";private long overtime = 1000 * 60 * 60;public String CreateToken(String userid, String username, List<String> roles) {long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);JwtBuilder builder = Jwts.builder().setId(userid).setSubject(username).setIssuedAt(now).signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);if (overtime > 0) {builder.setExpiration(new Date(nowMillis + overtime));}return builder.compact();}public boolean VerityToken(String token) {try {Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();if (claims != null) {return true;}} catch (Exception e) {e.printStackTrace();}return false;}public String getUserid(String token) {try {Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();if (claims != null) {return claims.getId();}} catch (Exception e) {e.printStackTrace();}return null;}public String getUserName(String token) {try {Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();if (claims != null) {return claims.getSubject();}} catch (Exception e) {e.printStackTrace();}return null;}public List<String> getUserRoles(String token) {try {Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();if (claims != null) {return (List<String>) claims.get("roles");}} catch (Exception e) {e.printStackTrace();}return null;}public String getClaims(String token, String param) {try {Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();if (claims != null) {return claims.get(param).toString();}} catch (Exception e) {e.printStackTrace();}return null;}
}

使用CreateToken就可以生成下面这种字符串:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4Iiwic3ViIjoiYWRtaW4iLCJpYXQiOjE2NDE3Mjg5NzgsInJvbGVzIjpbIlJPTEVfYWRtaW4iXSwiZXhwIjoxNjQxNzMyNTc4fQ.qI_pXiwm2IzZNRdyKvTRuSj0JxiPHepPOXg_u6AAE88

我们就使用类似上面这串做我们的Token。

既然使用Token了,肯定大多都是采用前后端分离架构,一般都是采用JSON进行交互的,但细心的会发现,前面的登录都是一个form表单的形式,所以第一步我们先把登录换成JSON的形式。

下面就需要重写自己的登录过滤器,需要实现UsernamePasswordAuthenticationFilter接口,其中attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 表示获取用户用户名密码的入口,我们可以在这里自定义接受用户名密码,比如以json形式接受,然后再传递给Security,然后Security下面就会去调用UserDetailsService做用户名和密码的正确性验证,如果用户名密码正确那就是登录成功,就会触发该实现下的successfulAuthentication方法,否则就是unsuccessfulAuthentication方法,我们可以在相应的方法中编写相应的提示返回给客户端:

public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {private JwtTool jwtTool;public LoginAuthenticationFilter(AuthenticationManager authenticationManager, JwtTool jwtTool) {this.setAuthenticationManager(authenticationManager);this.jwtTool = jwtTool;
//        this.setPostOnly(false);
//        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));}@SneakyThrows@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {response.setContentType("text/json;charset=utf-8");if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {BufferedReader br = null;try {br = new BufferedReader(new InputStreamReader(request.getInputStream(), "utf-8"));String line = null;StringBuilder sb = new StringBuilder();while ((line = br.readLine()) != null) {sb.append(line);}JSONObject json = JSONObject.parseObject(sb.toString());System.out.println(json.toString());String username = json.getString("username");String password = json.getString("password");UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);} catch (IOException e) {e.printStackTrace();response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("参数错误!").build()));}} else {return super.attemptAuthentication(request, response);}response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("参数错误!").build()));return null;}@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain, Authentication authentication) throws IOException, ServletException {UserEntity user = (UserEntity) authentication.getPrincipal();String username = user.getUsername();List<GrantedAuthority> authorities = (List<GrantedAuthority>) user.getAuthorities();List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());String token = jwtTool.CreateToken(String.valueOf(user.getId()), username, roles);response.setContentType("text/json;charset=utf-8");Map<String, Object> map = new HashMap<>();map.put("username", username);map.put("role", roles);map.put("token", token);response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().data(map).build()));}@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {response.setContentType("text/json;charset=utf-8");if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("用户名或密码错误!").build()));} else if (e instanceof DisabledException) {response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("账户被禁用,请联系管理员!").build()));} else {response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("用户名或密码错误!").build()));}}}

登录成功后,我们使用Jwt生成了一串Token并返还给用户,以后的所有请求都需要携带该Token,但Security 默认的是从Session中获取用户信息,显然也不符合我们的要求,所以下面我们要重写自己的Token过滤器。

需要实现BasicAuthenticationFilter接口,我们只需在doFilterInternal中做自己的逻辑即可,如果全部OK就放行该过滤器即可:

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {private JwtTool jwtTool;public JWTAuthenticationFilter(AuthenticationManager authenticationManager, JwtTool jwtTool) {super(authenticationManager);this.jwtTool = jwtTool;}public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {super(authenticationManager, authenticationEntryPoint);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {response.setContentType("text/json;charset=utf-8");String token = request.getHeader("token");if (StringUtils.isEmpty(token)){response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));return;}boolean isold = jwtTool.VerityToken(token);if (!isold) {response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));return;}String username = jwtTool.getUserName(token);if (StringUtils.isEmpty(username)) {response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));return;}List<String> roles = jwtTool.getUserRoles(token);if (roles.isEmpty()) {response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(403).message("权限不足!").build()));return;}List<GrantedAuthority> authorities = roles.stream().map(r -> new SimpleGrantedAuthority(r)).collect(Collectors.toList());UserEntity principal = new UserEntity();principal.setUsername(username);try {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(principal, null, authorities);SecurityContextHolder.getContext().setAuthentication(authentication);String userid = jwtTool.getUserid(token);request.setAttribute("userid", userid);request.setAttribute("username", username);
//            request.setAttribute("role", role);chain.doFilter(request, response);} catch (Exception e) {e.printStackTrace();response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));}}
}

上面我们重写了接受用户名密码,和校验Token的过滤器,显然已经符合我们前后端分离架构,但是还有一个就是无权限的返回,在上两篇就可以看出,无权限是返回的403错误,显然也不符合,应该要修改为JSON的返回。

我们可以实现AccessDeniedHandler这个接口,来做无权限自定义的返回:

@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)throws IOException, ServletException {response.setContentType("text/json;charset=utf-8");response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(403).message("权限不足!").build()));}}

最后修改WebSecurityConfig,将上面的过滤器添加到Security 中,替换到默认的过滤器:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredUserService userService;@AutowiredCustomAccessDecisionManager customAccessDecisionManager;@AutowiredCustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;@AutowiredAuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;@AutowiredJwtTool jwtTool;@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(password());}@BeanPasswordEncoder password() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O o) {o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);o.setAccessDecisionManager(customAccessDecisionManager);return o;}}).antMatchers("/**").fullyAuthenticated().and().formLogin().loginProcessingUrl("/login").permitAll().and().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler).and().addFilter(new JWTAuthenticationFilter(authenticationManager(), jwtTool)).addFilter(new LoginAuthenticationFilter(authenticationManager(),jwtTool)).csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/register/**");}@BeanRoleHierarchy roleHierarchy() {RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();String hierarchy = "ROLE_admin > ROLE_user > ROLE_common";roleHierarchy.setHierarchy(hierarchy);return roleHierarchy;}
}

相比于上一次的修改,这里就是通过addFilter的方式添加我们的过滤器。

下面就可以启动项目,访问http://localhost:8080/admin/test测试接口:

直接就是返回登录失效了,下面我们使用PostMan登录:http://localhost:8080/login

可以看到这里我把权限也返回出来进行测试,表示该用户只能访问admin/**,下面我们使用Token访问http://localhost:8080/admin/test

如果访问common/**:

到这里就实现了使用Jwt Token的认证授权了。


喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

本文标签: SpringSecurity