admin管理员组文章数量:1794759
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
版权声明:本文标题:SpringSecurity 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1692904951a220298.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论