集成了SpringSecurity后,又花了一天时间集成了JWT,记录一下。
完整代码在我的Gitee仓库:FreeFancy
HTTP是无状态的,我们不能确定两次请求是不是一个用户发出的,就必须要每次都进行认证。传统方法解决这个问题是采用session和cookie机制,虽然解决了问题,但有一些缺点:
- session通常是保存在内存中的,用户数量如果较多,服务器的压力大;
- 因为session是保存在最初认证的那台服务器上,换了一台服务器又需要认证,不能很好的适应分布式的环境;
- 有CSRF的风险。因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
后来人们提出了基于token的鉴权机制,它不需要服务器去记录用户的认证或会话信息,而是将token交给客户端保存,每次的请求的时候携带,服务器只要鉴定即可,这样做有一些优点:
- 服务器开销小;
- 不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利;
- 因为不依赖于session,就杜绝了CSRF的风险。
基于Token的鉴权机制应用最广的还是JWT(JSON WEB TOKEN)。这里我们不多解释JWT,相关文档很多请自动搜索。
1、实战 前置工作这里是在SpringBoot已经简单整合了SpringSecurity的情况下开始整合JWT,这里前置工作有:
因为是前后端分离的情况,我们统一返回JSON,所以实现了
- 登录成功的自定义处理;
- 登录失败的自定义处理;
- 未登录被拦截的自定义处理。
我们的用户信息是要在数据库中取的,所以我们要实现UserDetailsService接口,返回一个保存了用户信息的UserDetails对象,并在配置中注册。
部分配置文件
@Override protected void configure(HttpSecurity http) throws Exception { // 未登录拦截处理 http.exceptionHandling() .authenticationEntryPoint(customizeAuthenticationEntryPoint); // 登录 http.formLogin() //登录成功处理逻辑 .successHandler(customizeAuthenticationSuccessHandler) //登录失败处理逻辑 .failureHandler(customizeAuthenticationFailureHandler); // 登出 http.logout() //登出成功处理逻辑 .logoutSuccessHandler(customizeLogoutSuccessHandler); // 认证 http.authorizeRequests() // 公共接口放行 .antMatchers("/login", "/register").anonymous() // swagger静态资源放行 .antMatchers(SWAGGER_SOURCE_PERMIT_ALL.split(",")).permitAll() .anyRequest().authenticated(); }
这里贴出其中一个自定义处理,其他都是类似的,返回相应结果的JSON信息。
//登录被拦截自定义的处理 @Component public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException { ResponseJson responseJson = ResponseJson.failure(ResponseCode.USER_NOT_LOGGED_IN); httpServletResponse.setContentType("text/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseJson)); } }导包
思路io.jsonwebtoken jjwt0.9.1
看了很多别人的博客,思路并不清晰,开始还以为这东西搞起来很复杂,最后弄下其实还是不难的,需要配置的东西并不多。
- 对于新登录的用户,我们只要在登录成功后将TOKEN返回!,原本的逻辑并不需要修改,只要在上面我们自定义登录成功的处理中返回生成好的TOKEN。
- 对于登录过的用户发出的携带TOKEN的请求,我们要给它放行。
- 对于没有携带TOKEN的用户请求(要拦截的路径),给它打回去。
- JWT TOKEN的生成和解析。
思路是很自然的,得益于SpringSecurity实现起来也不难。
1、登录成功后返回TOKEN首先,/login路径的请求是不需要携带TOKEN就可以访问的。
http.authorizeRequests() // 公共接口放行 .antMatchers("/login", "/register").anonymous()
如果是表单登录,我们可以直接使用SpringSecurity自带的登录处理,请求参数中携带username,password,请求发给/login就可以了。
还记得我们的登录成功自定义处理嘛?我们在里面把token生成然后返回就可以了。
// 登录 http.formLogin() //登录成功处理逻辑 .successHandler(customizeAuthenticationSuccessHandler) //登录失败处理逻辑 .failureHandler(customizeAuthenticationFailureHandler);
@Component public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException { // 这里是生成TOKEN返回,有些方法还没讲,先理思路,知道是生成TOKEN即可! JwtUser jwtUser = (JwtUser) authentication.getPrincipal(); Collection extends GrantedAuthority> authorities = jwtUser.getAuthorities(); StringBuilder authoritiesStr = new StringBuilder(); for (GrantedAuthority authority : authorities) { authoritiesStr.append(authority.toString()); authoritiesStr.append(JwtTokenUtils.SPLIT_CHAR); } String token = JwtTokenUtils.createToken(jwtUser.getId(), jwtUser.getUsername(), authoritiesStr.toString()); //返回json数据 HashMaphashMap = new HashMap<>(1); hashMap.put("Authorization", token); ResponseJson responseJson = ResponseJson.success(hashMap); //处理编码方式,防止中文乱码的情况 httpServletResponse.setContentType("text/json;charset=utf-8"); //塞到HttpServletResponse中返回给前台 httpServletResponse.getWriter().write(JSON.toJSONString(responseJson)); } }
这样我们就完成了第一步。
2、携带TOKEN的请求,进行放行SpringSecurity本质是一个拦截器连(Filter Chain),我们需要自定义一个拦截器取处理请求中的TOKEN,并为请求放行。
public class JwtAuthenticationFilter extends BasicAuthenticationFilter { public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String headerToken = request.getHeader(JwtTokenUtils.TOKEN_HEADER); //如果请求头中没有token直接放行了,交给后续的拦截器处理。 if(headerToken == null || !headerToken.startsWith(JwtTokenUtils.TOKEN_PREFIX)){ chain.doFilter(request, response); return; } //请求头中有token且没过期 if(!JwtTokenUtils.isExpiration(headerToken.replace(JwtTokenUtils.TOKEN_PREFIX,""))){ //设置上下文,将认证的信息放进去,告诉后面的Filter不用再鉴权了! CustomizeUsernamePasswordAuthenticationToken authentication = getAuthentication(headerToken); SecurityContextHolder.getContext().setAuthentication(authentication); } super.doFilterInternal(request, response, chain); } //该方法就是从TOKNE中解析出用户的认证和鉴权信息 private CustomizeUsernamePasswordAuthenticationToken getAuthentication(String headerToken){ String token = headerToken.replace(JwtTokenUtils.TOKEN_PREFIX, ""); Long userId = JwtTokenUtils.getUserId(token); String username = JwtTokenUtils.getUserName(token); // 设置权限 String authoritiesStr = JwtTokenUtils.getUserAuthority(token); Listauthorities = new ArrayList<>(); for (String s : authoritiesStr.split(JwtTokenUtils.SPLIT_CHAR)) { if (s != null && !"".equals(s)) { authorities.add(new SimpleGrantedAuthority(s)); } } if(username != null){ return new CustomizeUsernamePasswordAuthenticationToken(userId, username, null, authorities); } return null; } }
我们要将这个Filter注册进SpringSecurity里,框架留有接口,很方便,但我们应该放在哪里呢?
// JWT http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
UsernamePasswordAuthenticationFilter是用来登录认证的,我们放在它前面,将TOKEN解析出的用户认证信息写进SecurityContext里,后续就可以放行了。
另外这里提一嘴,因为基于TOKEN的认证鉴权不依赖于session,所以我们可以把session禁用了,当然也可以大胆的把csrf防护关闭。
// 禁用session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.csrf().disable();3、拦截对于未携带TOKEN的请求
不是登录请求,又未携带TOKEN信息,会被后续的拦截器拦截,肯定是进不到系统的,后续拦截器报错,我们只要处理异常即可。
这里甚至不用特意处理,因为这和之前未登录拦截处理是一样的。
// 未登录拦截处理 http.exceptionHandling() .authenticationEntryPoint(customizeAuthenticationEntryPoint);
@Component public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException { ResponseJson responseJson = ResponseJson.failure(ResponseCode.USER_NOT_LOGGED_IN); httpServletResponse.setContentType("text/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(responseJson)); } }4、生成和解析TOKEN
生成是在成功登录后返回,解析是在处理携带TOKEN的请求,解析出用户的认证信息。
那哪些信息要放在TOKEN里呢?username和authorities(权限信息)以及你想要的信息。
我们对UserDetailsService的实现要求loadUserByUsername()返回一个UserDetails对象。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
自带的对象就可以,它是UserDetails的实现类。
org.springframework.security.core.userdetails.User
但它不能保存用户的ID,我就想把ID保存进TOKEN里,下次携带的时候可以解析出来,那我就自己写一个实现类。
public class JwtUser implements UserDetails { private final Long id; private String password; private final String username; private final Setauthorities; public JwtUser(Long id, String username, Set authorities){ this.id = id; this.username = username; this.authorities = authorities; } public JwtUser(Long id, String username, String password, Set authorities){ this.id = id; this.username = username; this.password = password; this.authorities = authorities; } public Long getId(){ return id; } @Override public Collection extends GrantedAuthority> getAuthorities(){ return authorities; } @Override public String getPassword(){ return password; } @Override public String getUsername(){ return username; } @Override public boolean isAccountNonExpired(){ return true; } @Override public boolean isAccountNonLocked(){ return true; } @Override public boolean isCredentialsNonExpired(){ return true; } @Override public boolean isEnabled(){ return true; } }
这个对象里的信息,就是我们要保存在TOKEN里的信息。
然后我们需要生成和解析TOKEN的一个工具类,这里直接给出我的。
public class JwtTokenUtils { public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; public static final String SECRET = "[email protected]"; public static final String SPLIT_CHAR = ";"; private static final Long EXPIRATION = 60 * 60 * 3L; private static final String AUTHORITY = "authority"; private static final String ID = "id"; public static String createToken(Long id, String username, String authority){ HashMapmap = new HashMap<>(2); map.put(ID, id); map.put(AUTHORITY, authority); return TOKEN_PREFIX + Jwts.builder() .signWith(SignatureAlgorithm.HS512, SECRET) .setClaims(map) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000)) .compact(); } public static Long getUserId(String token){ Long id; try { id = (Long) getTokenBody(token).get(ID); } catch (Exception e){ id = null; } return id; } public static String getUserName(String token){ String username; try { username = getTokenBody(token).getSubject(); } catch (Exception e){ username = null; } return username; } public static String getUserAuthority(String token){ return (String) getTokenBody(token).get(AUTHORITY); } private static Claims getTokenBody(String token){ return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } public static boolean isExpiration(String token){ return getTokenBody(token).getExpiration().before(new Date()); } }
生成TOKEN用在:
@Component public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException { JwtUser jwtUser = (JwtUser) authentication.getPrincipal(); Collection extends GrantedAuthority> authorities = jwtUser.getAuthorities(); StringBuilder authoritiesStr = new StringBuilder(); for (GrantedAuthority authority : authorities) { authoritiesStr.append(authority.toString()); authoritiesStr.append(JwtTokenUtils.SPLIT_CHAR); } String token = JwtTokenUtils.createToken(jwtUser.getId(), jwtUser.getUsername(), authoritiesStr.toString()); //返回json数据 HashMaphashMap = new HashMap<>(1); hashMap.put("Authorization", token); ResponseJson responseJson = ResponseJson.success(hashMap); //处理编码方式,防止中文乱码的情况 httpServletResponse.setContentType("text/json;charset=utf-8"); //塞到HttpServletResponse中返回给前台 httpServletResponse.getWriter().write(JSON.toJSONString(responseJson)); } }
解析TOKEN用在:
public class JwtAuthenticationFilter extends BasicAuthenticationFilter { public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String headerToken = request.getHeader(JwtTokenUtils.TOKEN_HEADER); //如果请求头中没有token直接放行了 if(headerToken == null || !headerToken.startsWith(JwtTokenUtils.TOKEN_PREFIX)){ chain.doFilter(request, response); return; } //请求头中有token且没过期 if(!JwtTokenUtils.isExpiration(headerToken.replace(JwtTokenUtils.TOKEN_PREFIX,""))){ //设置上下文 CustomizeUsernamePasswordAuthenticationToken authentication = getAuthentication(headerToken); SecurityContextHolder.getContext().setAuthentication(authentication); } super.doFilterInternal(request, response, chain); } private CustomizeUsernamePasswordAuthenticationToken getAuthentication(String headerToken){ String token = headerToken.replace(JwtTokenUtils.TOKEN_PREFIX, ""); Long userId = JwtTokenUtils.getUserId(token); String username = JwtTokenUtils.getUserName(token); // 设置权限 String authoritiesStr = JwtTokenUtils.getUserAuthority(token); Listauthorities = new ArrayList<>(); for (String s : authoritiesStr.split(JwtTokenUtils.SPLIT_CHAR)) { if (s != null && !"".equals(s)) { authorities.add(new SimpleGrantedAuthority(s)); } } if(username != null){ return new CustomizeUsernamePasswordAuthenticationToken(userId, username, null, authorities); } return null; } }
因为SecurityContextHolder.getContext().setAuthentication();要求传入Authentication的实现了,但框架自带的并不能携带我们要求携带的userId,所以自定义一个实现类。
public class CustomizeUsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private final Long userId; private final Object principal; private Object credentials; public CustomizeUsernamePasswordAuthenticationToken(Long userId, Object principal, Object credentials) { super(null); this.userId = userId; this.principal = principal; this.credentials = credentials; setAuthenticated(false); } public CustomizeUsernamePasswordAuthenticationToken(Long userId, Object principal, Object credentials, Collection extends GrantedAuthority> authorities) { super(authorities); this.userId = userId; this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } public Long getUserId() { return this.userId; } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }5、使用SecurityContext中的信息
每次请求,SecurityContext中保有我们放入的一小段信息,这份信息可以在该次请求的时候拿出来。
public class SecurityUtils { public static Long getUserId(){ Object token = SecurityContextHolder.getContext().getAuthentication(); if (token instanceof CustomizeUsernamePasswordAuthenticationToken) { return ((CustomizeUsernamePasswordAuthenticationToken) token).getUserId(); } return null; } }
到这里就完成了集成,捋清了思路其实还是很简单的,可以根据业务调整的地方有很多,可以在理解原理后按需修改。
如有错误,恳请指出!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)