11、整合JWT
JWT是JSON Web Token的缩写,即JSON Web令牌。JSON Web令牌(JWT)是一种紧凑的、URL安全的方式,用来表示要在双方之间传递的“声明”。JWT中的声明被编码为JSON对象,用作JSON Web签名(JWS)结构的有效内容或JSON Web加密(JWE)结构的明文,使得声明能够被:数字签名、或利用消息认证码(MAC)保护完整性、加密。
JWT构成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与 签名依顺序用点号(".")链接而成:header,payload,signature
Header
头部(Header)里面说明类型和使用的算法,比如:
{
"alg": "HS256",
"typ": "JWT"
}
说明是JWT(JSON web token)类型,使用了HMAC SHA 算法。然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload
载荷(Payload)载荷就是存放有效信息的地方,含三个部分:
1、标准中注册的声明
2、公共的声明
3、私有的声明
1、标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2 、公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
3、私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
把场景2的操作描述成一个json对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT。 当然,你还可以往载荷放非敏感的用户信息,比如uid
signature
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
//TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
应用场景
应用场景:
1、浏览器将用户名和密码以post请求的方式发送给服务器。
2、服务器接受后验证通过,用一个密钥生成一个JWT。
3、服务器将这个生成的JWT返回给浏览器。
4、浏览器存储JWT并在使用时将JWT包含在authorization header里面,然后发送请求给服务器。
5、服务器可以在JWT中提取用户相关信息。进行验证。
6、服务器验证完成后,发送响应结果给浏览器。
好吹就是无状态,在前后端分离的应用中,后台不需要存储状态,减轻服务器的压力。
整合
这个又是借鉴了github上一位大神的代码,其实我在好几个地方看大了那个代码了,需要引入一个jar,用于处理JWT的一些操作
<!-- JWT支持 -->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
整体思路如下:
在 UsernamePasswordAuthenticationFilter 过滤器前添加一个过滤器,该过滤器主要用是针对 JWT 的认证?就是说,当前端的请求头中,包含了token信息,就通过自定义过滤器逻辑走认证过程,认证 通过之后,把认证信息交给spring-security上下文,然后继续走spring-security的流程,就相当于如果前端请求头总有token信息,并且后台校验这个token信息是没有问题的,就让spring-security认为这个请求时已经通过校验的,很巧妙。如果没有包含token,就执行 spring-security标准的认证流程。然后开放一个用于前端请求token的接口,这个接口不需要认证,认证通过之后,返回前端一个token,前端可以保存到 localstorage里面,也可以保存到cookie里面。然后下次请求的时候,在请求头中带上这个token信息。
下面主要时贴一下核心代码,项目源码都已经上传了,可以自己看你看。
//security配置类
package com.hand.sxy.config;
import com.hand.sxy.jwt.JwtAuthenticationEntryPoint;
import com.hand.sxy.jwt.JwtAuthorizationTokenFilter;
import com.hand.sxy.jwt.JwtTokenUtil;
import com.hand.sxy.security.CustomUserService;
import com.hand.sxy.security.MyFilterSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* @author spilledyear
* @date 2018/4/24 13:19
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.route.authentication.path}")
private String authenticationPath;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
/**
* 通过这种方式注入 authenticationManagerBean ,然后在别的地方也可以用
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 注册 UserDetailsService 的 Bean
*
* @return
*/
@Bean
UserDetailsService customUserService() {
return new CustomUserService();
}
/**
* Sring5 中密码加密新方式
*
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// @Bean
// RememberMeServices rememberMeServices() {
// SpringSessionRememberMeServices rememberMeServices = new SpringSessionRememberMeServices();
// rememberMeServices.setAlwaysRemember(true);
// return rememberMeServices;
// }
// @Bean
// CorsConfigurationSource corsConfigurationSource() {
// CorsConfiguration configuration = new CorsConfiguration();
// configuration.setAllowedOrigins(Arrays.asList("http://localhost:8082"));
// configuration.addAllowedHeader("*");
// configuration.setAllowedMethods(Arrays.asList("POST, GET, OPTIONS, PUT, DELETE"));
// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// source.registerCorsConfiguration("/**", configuration);
// return source;
// }
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// httpSecurity.rememberMe().rememberMeServices(rememberMeServices());
// httpSecurity.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
httpSecurity
.cors().and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
/** 不创建 session **/
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/*.html", "/**/*.html", "/**/*.js", "/**/*.css").permitAll()
.antMatchers("/login", "/register", "/auth", "/oauth/*").permitAll()
.antMatchers("/api/role/query").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").loginProcessingUrl("/api/system/login").usernameParameter("username").passwordParameter("password").permitAll()
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/api/system/logout").permitAll();
/**
* spring security过滤器链中,真正的用户信息校验是 UsernamePasswordAuthenticationFilter 过滤器,然后才是权限校验。
* 这里在 UsernamePasswordAuthenticationFilter过滤器之前 自定义一个过滤器,这样就可以提前根据token将authenticate信息
* 维护进speing security上下文,然后在 UsernamePasswordAuthenticationFilter 得到的就已经是通过校验的用户了。
*/
JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(customUserService(), jwtTokenUtil, tokenHeader);
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
/**
* disable page caching
*
* 下面这行代码巨玄乎,加了这个之后,前端应用就无法正常访问了(也就是说需要开发/api/**权限才能正常 访问)
*/
// httpSecurity.headers().frameOptions().sameOrigin().cacheControl();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/**
* userDetailService验证
*/
// auth.userDetailsService(customUserService()).passwordEncoder(new PasswordEncoder() {
//
// @Override
// public String encode(CharSequence rawPassword) {
// return rawPassword.toString();
// }
//
// @Override
// public boolean matches(CharSequence rawPassword, String encodedPassword) {
// return encodedPassword.equals(rawPassword.toString());
// }
// });
auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity webSecurity) {
webSecurity
.ignoring().antMatchers(HttpMethod.POST, "/login", "/auth")
.and()
.ignoring().antMatchers("/**/*.html", "/**/*.js", "/**/*.css");
}
}
注意上面的
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
package com.hand.sxy.jwt;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
这表示当你权限认证失败的时候,执行 JwtAuthenticationEntryPoint 里面的 commence方法,返回给前端,用于自定义适合客户阅读的提示。
这个就是开放的接口,用于前端请求token信息。
/**
* 认证接口,用于前端获取 JWT 的接口
*
* @param user
* @return
* @throws AuthenticationException
*/
@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
@ResponseBody
public ResultResponse obtainToken(@RequestBody User user) throws AuthenticationException {
/**
* 通过调用 spring security 中的 authenticationManager 对用户进行验证
*/
Objects.requireNonNull(user.getUsername());
Objects.requireNonNull(user.getPassword());
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
} catch (DisabledException e) {
throw new AuthenticationException("该已被被禁用,请检查", e);
} catch (BadCredentialsException e) {
throw new AuthenticationException("无效的密码,请检查", e);
}
/**
* 根据用户名从数据库获取用户信息,然后生成 token
*/
final UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
List<User> userList = userService.query(user);
ResultResponse resultSet = new ResultResponse(true, token);
resultSet.setRows(userList);
return resultSet;
}
//这个时jwt工具类
package com.hand.sxy.jwt;
import com.hand.sxy.security.CustomUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* @author spilledyear
*/
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
private Clock clock = DefaultClock.INSTANCE;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getIssuedAtDateFromToken(String token) {
return getClaimFromToken(token, Claims::getIssuedAt);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
private Boolean ignoreTokenExpiration(String token) {
// here you specify tokens, for that the expiration is ignored
return false;
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getIssuedAtDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token) || ignoreTokenExpiration(token));
}
public String refreshToken(String token) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
CustomUser customUser = (CustomUser) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
//final Date expiration = getExpirationDateFromToken(token);
return (
username.equals(customUser.getUsername()) && !isTokenExpired(token) && !isCreatedBeforeLastPasswordReset(created, customUser.getLastPasswordResetDate())
);
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration * 1000);
}
}
//这个就是关键的过滤器了
package com.hand.sxy.jwt;
import io.jsonwebtoken.ExpiredJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author spilledyear
*/
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private String tokenHeader;
public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
logger.debug("processing authentication for '{}'", request.getRequestURL());
final String token = request.getHeader(this.tokenHeader);
String username = null;
if (token != null && !"".equals(token)) {
try {
username = jwtTokenUtil.getUsernameFromToken(token);
} catch (IllegalArgumentException e) {
logger.error("从Token中获取用户名失败", e);
} catch (ExpiredJwtException e) {
logger.warn("这个Token已经失效了", e);
}
} else {
logger.warn("请求头中未发现 Token, 将执行Spring Security正常的验证流程");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
logger.debug("security context was null, so authorizating user");
// 也可以将用户信息保存在token中,这时候就可以不用查数据库
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 校验前端传过来的Token是否有问题
if (jwtTokenUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("用户 '{}' 授权成功, 赋值给 SecurityContextHolder 上下文", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}