JWT
认证机制
HTTP Basic Auth
HTTP Basic Auth 简单点说,就是每次请求 API 时都提供用户的 username 和 password,简言之,Basic Auth 是配合 RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的 RESTfulAP I时,尽量避免采用 HTTP Basic Auth。
Cookie Auth
Cookie 认证机制就是为一次请求认证在服务端创建一个 Session 对象,同时在客户端的浏览器端创建了一个 Cookie 对象。通过客户端带上来 Cookie 对象来与服务器端的Session 对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,Cookie 会被删,但可以通过修改 Cookie 的 expire time 使 cookie 在一定时间内有效。工作原理图如下:
OAuth 2.0
OAuth 2.0 (Open Authorization,开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一 web 服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用,例如网站通过微信、微博登录等,主要用于第三方登录。
OAuth 2.0 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
Token Auth
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程就是:
-
客户端使用用户名跟密码请求登录。
-
服务端收到请求,去验证用户名与密码。
-
验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端。
-
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里。
-
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token。
-
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。
这种方式比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量,也就是技术不断的演进的过程。具体的, Token Auth 的优点(Token机制相对于Cookie机制又有什么好处呢? )
-
支特跨域访问。Cookie 是不允许垮域访问的,这一点对 Token 机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
-
无状态(也称:服务端可扩展行)。Token 机制在服务端不需要存储 Session 信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的 Cookie 或本地介质存储状态信息。
-
更适用 CDN。可以通过内容分发网络情求你服务端的所有资料(如: javascript,HTML.图片等),而你的服务端只要提供API即可。
-
去耦。不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
-
更适用于移动应用。当你的客户端是一个原生平台(iOS , Android ,Windows 10等)时,Cookie 是不被支持的(你需要通过 Cookie 容器进行处理),这时采用 Token 认证机制就会简单得多。
-
CSRF。因为不再依赖于 Cookie,所以你就不需要考虑对 CSRF(跨站请求伪造)的防范。
-
性能。一次网络往返时间(通过数据库查间session信息)总比做一次 HMACSHA256 计算的 Token 验证和解析要费时得多。
-
不需要为登录页面做特殊处理。如果你使用 Protractor 做功能测试的时候,不再需要为登录页面做特殊处理,基于标准化,你的 API 可以采用标准化的 JSON Web Token (JWT)。这个标准已经存在多个后端库(.NET,Ruby,Java,Python,PHP)和多家公司的支持(如: Firebase,Google, Microsoft)
JWT 简介
什么是 JWT
JWT(JSON Web Token) 是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递 JSON 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公钥/私钥对来签名,防止被篡改。
优点
-
jwt 基于 json,非常方便解析。
-
可以在令牌中自定义丰富的内容,易扩展。
-
通过非对称加密算法及数字签名技术,JWT 防止篡改,安全性高。
-
资源服务使用 JW T可不依赖认证服务即可完成授权。
缺点: JWT 令牌较长,占存储空间比较大。
JWT 的组成
头部(Header)
头部用于描述关于该 JWT 的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA)等。这也可以被表示成一个JSON对象,其中 type 是指类型,alg 是指签名的算法。
{
"alg": "HS256",
"typ": "JWT"
}
我们对头部的 JSON 字符串进行 BASE64 编码(网上有很多在线编码的网站),编码后的字符串如下:
ey -> JhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9
Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码。注意,这里只是进行编码,并不是加密,Base64 无任何加密效果,SHA256 的摘要只是为 JSON 数据生成一个“指纹”,防止被篡改,属于完整性范畴,也无任何加密效果,摘要不等于签名,签名是用私钥加密摘要。所以 Token 本身并没有任何加密机制,它依赖于 HTTPS 的通道保密能力。
负载(Payload)
第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分,标准声明、公共声明以及私有声明,分别如下:
- 标准中注册的声明(建议但不强制使用)
iss:.jwt 签发者
sub: jwt 所面向的用户
aud: 接牧 jwt 的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
-
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。 -
私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的 claim。比如下面那个举例中的 name 就属于自定的 claim。这些 claim 跟 JWT 标准规定的claim 区别在于:JWT规定的 claim,JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证);而自定义的 claims 不会验证,除非明确告诉接收方要对这些 claim 进行验证以及规则才行。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
签证、签名(signature)
JWT 的第三部分是一个签证信息,这个签证信息由三部分组成:header、payload、secret(盐,一定要保密)。这个部分需要 base64 编码后的 header 和 payload 使用,连接组成的字符串,然后通过 header 中声明的编码方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分,将这三部分用“.”连接成一个完整的字符串,最终构成了 JWT。
注意: secret 是保存在服务器端的, JWT 的签发生成也是在服务器端的, secret 就是用来进行 JWT 的签发和 JWT 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret , 那就意味着客户端是可以自我签发jwt了。
下图是 JWT 的组成信息:
JWT 使用
工具类封装
@Component
public class TokenService {
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
@Autowired
private RedisCache redisCache;
@Autowired
private TokenProperties tokenProperties;
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
// 解析 Token ,从令牌中获取自定义数据声明
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
return redisCache.getCacheObject(userKey);
} catch (Exception ignored) {
}
}
return null;
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser) {
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
refreshToken(loginUser);
}
}
/**
* 删除用户身份信息
*/
public void delLoginUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
String token = IdUtil.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser); // 设置用户代理信息
refreshToken(loginUser); // 刷新令牌有效期
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token); // 设置自定义声明令牌前缀,login_user_key
return createToken(claims); // 创建令牌
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser 用户信息
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
// 设置登录时间
loginUser.setLoginTime(System.currentTimeMillis());
// 设置过期时间
loginUser.setExpireTime(loginUser.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, tokenProperties.getExpireTime(), TimeUnit.MINUTES);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims) {
return Jwts.builder()
// 设置自定义声明
.setClaims(claims)
// 设置签名算法和秘钥
.signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret()).compact();
}
/**
* 解析 Token ,从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parser()
// 设置自定义的令牌秘钥
.setSigningKey(tokenProperties.getSecret())
// 将 Token 转换为自定义声明
.parseClaimsJws(token)
.getBody();
}
/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOs().getName());
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request) {
// 获取自定义令牌标识,并且匹配到默认的 "Bearer ",则替换掉请求前缀,否则直接返回自定义令牌标识
String token = request.getHeader(tokenProperties.getHeader());
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
private String getTokenKey(String uuid) {
return Constants.LOGIN_TOKEN_KEY + uuid;
}
}
配置 JWT 过滤器,验证 Token 的合法性
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 获取用户身份信息,验证 Token 的合法性
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
// 验证令牌有效期,相差不足20分钟,自动刷新缓存
tokenService.verifyToken(loginUser);
// 比对用户信息,验证用户是否合法
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 填充主体信息,存入上下文环境
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
SpringSecurity 配置 JWT
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").anonymous()
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated().and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
}
接口权限验证
@Service("ss")
public class PermissionService {
@Autowired
private TokenService tokenService;
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
// 获取用户身份信息,此方法进行了一系列Token工作
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
}
@Validated
@Api(value = "测试单表控制器", tags = {"测试单表管理"})
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@RestController
@RequestMapping("/demo/demo")
public class TestDemoController extends BaseController {
private final ITestDemoService iTestDemoService;
/**
* 查询测试单表列表
*/
@ApiOperation("查询测试单表列表")
@PreAuthorize("@ss.hasPermi('demo:demo:list')")
@GetMapping("/list")
public TableDataInfo<TestDemoVo> list(@Validated TestDemoBo bo) {
return iTestDemoService.queryPageList(bo);
}
}