SpringSecurity+JWT结合Vue在前后端分离下的权限控制 前言 再构建一个前后端分离项目的时候。我们之前在前后端耦合情况下使用的Security配置会和前后端分离的情况下有一些较大的区别。
就比如在前后端耦合的情况下,所有页面。都是由后端来处理,这样的权限控制较为简易。而且不会出现跨域的问题。按目前结合vue进行前后端分离是当前技术的主流。很多公司和业务都是按照着这个的标准进行的,那么以往的权限控制就不能完全适用于现在了。
这一段设计经过了我的一些思考。当我在初步的使用vue的时候。我还按照原来的配置去配置security。结果发现这样的权限控制并没有任何效果。起初以为是跨域问题,结果在解决了跨域问题之后,才真正明白了,他们启动的并不是同一个端口。那么我在后端配置的权限控制和前端不同端口的情况下怎么会生效呢?
于是在查阅了网上的各种资料之后,我找到了一种方法,使用jwt配合security权限控制来进行前后端之间的交流,这样即使是在不同端口的情况下也能实现权限的控制。并且可以将jwt所认证的信息存储在当地的cookie当中。即使用户退出的浏览器在打开浏览器时也不用再次进行登录去认证。
准备工作 首先必不可说的肯定是依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
因为在进行前后端分离时,需要用到一个自定义的类型去进行交互。当然这是基本常识,在此也不多讲述:
这是一个用于交互的实体,前后端用这个类来进行交互。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Data public class Result implements Serializable { private int code; private String msg; private Object data; public static Result succ (Object data) { return succ(200 , "操作成功" , data); } public static Result succ (int code, String msg, Object data) { Result r = new Result(); r.setCode(code); r.setMsg(msg); r.setData(data); return r; } public static Result fail (String msg) { return fail(400 , msg, null ); } public static Result fail (String msg, Object data) { return fail(400 , msg, data); } public static Result fail (int code, String msg, Object data) { Result r = new Result(); r.setCode(code); r.setMsg(msg); r.setData(data); return r; } }
我们还需要定义一个用户提交登陆信息使用的类
1 2 3 4 5 6 7 8 9 @Data public class LoginDto implements Serializable { @NotBlank (message = "昵称不能为空" ) private String username; @NotBlank (message = "密码不能为空" ) private String password; }
重点 后端需要解决一些跨域问题,这是一个跨域解决方案的一个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ) .allowedOrigins("*" ) .allowedMethods("GET" , "HEAD" , "POST" , "PUT" , "DELETE" , "OPTIONS" ) .allowCredentials(true ) .maxAge(3600 ) .allowedHeaders("*" ); } }
开始配置我们的security:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity (prePostEnabled=true )public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MyUserDetailsService myUserDetailsService; public JwtTokenFilter authenticationTokenFilterBean () throws Exception { return new JwtTokenFilter(); } @Bean public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); } @Override protected void configure ( AuthenticationManagerBuilder auth ) throws Exception { auth.userDetailsService( myUserDetailsService ).passwordEncoder( new BCryptPasswordEncoder() ); } @Override protected void configure ( HttpSecurity httpSecurity ) throws Exception { httpSecurity.csrf() .disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, "/**" ).permitAll() .antMatchers(HttpMethod.POST, "/login" ).permitAll() .antMatchers(HttpMethod.POST, "/register" ).permitAll() .antMatchers(HttpMethod.POST).authenticated() .antMatchers(HttpMethod.PUT).authenticated() .antMatchers(HttpMethod.DELETE).authenticated() .antMatchers(HttpMethod.GET).authenticated() .and() .cors() ; httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class ) ; httpSecurity.headers().cacheControl(); } }
MyUserDetailsService:这个不用说,就是我们自己定义的获取数据库用户信息的方法。接着使用: auth.userDetailsService( myUserDetailsService ).passwordEncoder( new BCryptPasswordEncoder() ); 将他住到我们的security权限认证当中。
JwtTokenFilter:这是我们的拦截器。我们需要对验证的每一个Token.进行验证,判断它的过期时间等等。我们需要把这个拦截器一块注入到security当中。源码在下方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Component public class JwtTokenFilter extends OncePerRequestFilter { @Qualifier ("myUserDetailsService" ) @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader( Const.HEADER_STRING ); if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) { final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() ); String username = jwtTokenUtil.getUsernameFromToken(authToken); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null ) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null , userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
接着这一个认证拦截器又用到了JWT的工具类:这一个工具,通常用于生成JWT,和获取各种JWT的信息,其中用到了我们spring中的依赖注的JWT源码,但我们还需要根据他所给的方法和API,进行自己的改写和判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 @Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -5625635588908941275L ; private static final String CLAIM_KEY_USERNAME = "sub" ; private static final String CLAIM_KEY_CREATED = "created" ; public String getUsernameFromToken (String token) { String username; try { final Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null ; } return username; } public Date getCreatedDateFromToken (String token) { Date created; try { final Claims claims = getClaimsFromToken(token); created = new Date((Long) claims.get(CLAIM_KEY_CREATED)); } catch (Exception e) { created = null ; } return created; } public Date getExpirationDateFromToken (String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null ; } return expiration; } private Claims getClaimsFromToken (String token) { Claims claims; try { claims = Jwts.parser() .setSigningKey( Const.SECRET ) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null ; } return claims; } private Date generateExpirationDate () { return new Date(System.currentTimeMillis() + Const.EXPIRATION_TIME * 1000 ); } private Boolean isTokenExpired (String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } private Boolean isCreatedBeforeLastPasswordReset (Date created, Date lastPasswordReset) { return (lastPasswordReset != null && created.before(lastPasswordReset)); } public String generateToken (UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } String generateToken (Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, Const.SECRET ) .compact(); } public Boolean canTokenBeRefreshed (String token) { return !isTokenExpired(token); } public String refreshToken (String token) { String refreshedToken; try { final Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null ; } return refreshedToken; } public Boolean validateToken (String token, UserDetails userDetails) { User user = (User) userDetails; final String username = getUsernameFromToken(token); return ( username.equals(user.getUsername()) && !isTokenExpired(token) ); } }
在此,这种工作大致上就完成了,总体而言并不复杂。就是把JWT和security结合在一起。
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @PostMapping ("/login" )public Result login (@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) { System.out.println("登录" ); String jwt= authService.login( loginDto.getUsername(), loginDto.getPassword() ); User user= userService.findByUsername(loginDto.getUsername()).get(0 ); response.setHeader("Authorization" ,jwt); response.setHeader("Access-control-Expose-Headers" ,"Authorization" ); return Result.succ(MapUtil.builder() .put("id" , user.getId()) .put("username" , user.getUsername()) .map()); }
使用postman或者是vue的axios进行测试,图床崩了暂时就不搞图了