Token 验证
在这之前先简单讲下登陆中验证码认证过程,先是在WebSecurityConfig里面配置下生成验证码的路径,使用谷歌的com.google.code.kaptcha API中的Producer对象生成验证码,后台生成Cookie 对象,key可以自定义,值用uuid去生成,然后把验证码存到redis中(key用的刚才uuid生成的那个值,value存验证码),然后把Cookie 返给前端。登录的时候传验证码过来,并且把刚才返的Cookie带过来,校验就根据cookie从redis中去取验证码。
public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setHeader(“Cache-Control”, “no-store, no-cache”);
response.setContentType(“image/jpeg”);
//生成文字验证码
String text = producer.createText();
//生成图片验证码
BufferedImage image = producer.createImage(text);
//保存session
String uuid = UUID.randomUUID().toString();
Cookie cookie = new Cookie(SystemConstants.KAPTCHA_SESSION_KEY, uuid);
response.addCookie(cookie);
redisUtils.set(uuid, text);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
}
使用基于 Token 的身份验证方法,大概的流程是这样的:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
总的来说就是客户端在首次登陆以后,服务端再次接收http请求的时候,就只认token了,请求只要每次把token带上就行了,服务器端会拦截所有的请求,然后校验token的合法性,合法就放行,不合法就返回401(鉴权失败)。
乍的一看好像和前面的seesion-cookie有点像,seesion-cookie是通过seesionid来作为浏览器和服务端的链接桥梁,而token验证方式貌似是token来起到seesionid的角色。其实这两者差别是很大的。
- sessionid 他只是一个唯一标识的字符串,服务端是根据这个字符串,来查询在服务器端保持的seesion,这里面才保存着用户的登陆状态。但是token本身就是一种登陆成功凭证,他是在登陆成功后根据某种规则生成的一种信息凭证,他里面本身就保存着用户的登陆状态。服务器端只需要根据定义的规则校验这个token是否合法就行。
- session-cookie是需要cookie配合的,居然要cookie,那么在http代理客户端的选择上就是只有浏览器了,因为只有浏览器才会去解析请求响应头里面的cookie,然后每次请求再默认带上该域名下的cookie。但是我们知道http代理客户端不只有浏览器,还有原生APP等等,这个时候cookie是不起作用的,或者浏览器端是可以禁止cookie的(虽然可以,但是这基本上是属于吃饱没事干的人干的事)…,但是token 就不一样,他是登陆请求在登陆成功后再请求响应体中返回的信息,客户端在收到响应的时候,可以把他存在本地的cookie,storage,或者内存中,然后再下一次请求的请求头重带上这个token就行了。简单点来说cookie-session机制他限制了客户端的类型,而token验证机制丰富了客户端类型。
- 时效性。session-cookie的sessionid实在登陆的时候生成的而且在登出事时一直不变的,在一定程度上安全就会低,而token是可以在一段时间内动态改变的。
- 可扩展性。token验证本身是比较灵活的,一是token的解决方案有许多,常用的是JWT,二来我们可以基于token验证机制,专门做一个鉴权服务,用它向多个服务的请求进行统一鉴权。
下面就拿最常用的JWT(JSON WEB TOKEN)来说:
JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,就是登陆成功后将相关信息组成json对象,然后对这个对象进行某中方式的加密,返回给客户端,客户端在下次请求时带上这个token,服务端再收到请求时校验token合法性,其实也就是在校验请求的合法性。
具体实现如下:
调用登录接口,先是进入拦截器匹配url,
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String servletPath = request.getServletPath();
boolean isLogin= pathMatcher.match("/login", servletPath);
if (isNoPermission(servletPath,"/swagger-ui.html","/swagger-resources/**","/v2/**", "/webjars/**","/api/voucher/download/**","/project/sourceAttach/download/**")){
filterChain.doFilter(request, response);
return;
}
if(!isLogin){
boolean isCaptcha=pathMatcher.match("/code/**", servletPath);
if(!isCaptcha){
validToken(request,response);
}
}else{
/**
* 验证码校验
*/
if (!"dev".equals(environment)){
validKaptcha(request);
}
}
}catch (BadCredentialsException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
filterChain.doFilter(request, response);
}
通过后就进行调用登陆器
/**
-
自定义登录登录器,使用JSON登录
*/
public class LoginAuthFilter extends UsernamePasswordAuthenticationFilter {private static final Logger LOGGER = LoggerFactory.getLogger(LoginAuthFilter.class);
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if ( !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);
}
接着进行登录认证
/**
-
登录认证逻辑
*/
public class LoginAuthenticationManager implements AuthenticationManager {private static final Logger LOGGER = LoggerFactory.getLogger(LoginAuthenticationManager.class);
private UserService userService;
public LoginAuthenticationManager(UserService userService){
this.userService=userService;
}@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {if(authentication.getPrincipal() == null || authentication.getCredentials() == null) { throw new BadCredentialsException("登陆验证失败,用户名或密码为空"); } String username = (String)authentication.getPrincipal(); //查询用户信息 User user=userService.loadUserByUsername(username); if(user==null){ throw new BadCredentialsException("账号信息不存在"); } String password = (String)authentication.getCredentials(); Boolean passwordTrue=new BCryptPasswordEncoder().matches(password,user.getPassword()); //比较密码是否一致 if(!passwordTrue){ throw new BadCredentialsException("密码不正确"); } return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), user.getAuthorities());
}
}
认证通过后就进入了自定义的 登录认证成功的handler进行生成token操作,并把token存进redis,然后返回给前端,这里还可以把用户相关信息封装好根据需要返给前端
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
User sysUserEntity=(User) authentication.getPrincipal();
User sysUser=new User();
sysUser.setUserId(sysUserEntity.getUserId());
sysUser.setUserName(sysUserEntity.getUserName());
sysUser.setLoginUserName(sysUserEntity.getLoginUserName());
//创建token
String token= JwtTokenUtil.createJWT(sysUserEntity);
redisUtils.set(token+sysUserEntity.getUserId(),token);
//设置一下用户登录信息到redis中去
redisUtils.set(sysUserEntity.getUserId(), JSON.toJSONString(sysUserEntity));
JSONObject data = new JSONObject();
data.put("code", SystemConstants.SUCCESS);
data.put("msg", "登陆成功");
data.put("data", sysUser);
httpServletResponse.setHeader(SystemConstants.AUTHORIZATION,token);
response(httpServletResponse, data);
}
对应的生成token逻辑,这里使用jwt生成的
/**
* 构建jwt
* @param user
* @param
* @return
*/
public static String createJWT(User user) {
try {
// 使用HS256加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(BASE64_BIARY);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
// 可以将基本不重要的对象信息放到claims
.setSubject(user.getUserName()) // 代表这个JWT的主体,即它的所有人
.setIssuedAt(new Date()) // 是一个时间戳,代表这个JWT的签发时间;
.setClaims(accountEntityfillMap(user))
.signWith(signatureAlgorithm, signingKey);
//生成JWT
return builder.compact();
} catch (Exception e) {
log.error("签名失败", e);
throw e;
}
}
public static Map<String, Object> accountEntityfillMap(User user) throws NullPointerException {
Map<String, Object> info = new WeakHashMap<>();
info.put("loginUserName", user.getLoginUserName());
info.put("userId", user.getUserId());
info.put("exTime",new Date());
return info;
}
然后前端把登陆成功返的token存起来,后面每次调用接口的时候带上就可以了,调用接口的时候会先进拦截器,然后就是校验token逻辑
private void validToken(HttpServletRequest request, HttpServletResponse response) throws BadCredentialsException{
boolean hasPower = false;
//获取请求头
String token=request.getHeader(SystemConstants.AUTHORIZATION);
if(ObjectMapper==null){
ObjectMapper = new ObjectMapper();
}
try {
if(token ==null){
throw new BadCredentialsException("token is empty");
}
//解析token
Claims claims=JwtTokenUtil.parseJWT(token);
String userId=claims.get("userId",String.class);
if (userId == null || SecurityContextHolder.getContext().getAuthentication() == null) {
//判断是否过期
if(redisUtils.get(userId)==null){
throw new BadCredentialsException("token 已过期");
}
//获取redis中的用户信息
String userJson=redisUtils.get(userId);
User user = JSON.parseObject(userJson,User.class);
//判断权限
if(user!=null && CollectionUtils.isEmpty(user.getPermissions())){
throw new BadCredentialsException("当前请求没有权限,请联系管理员");
}
if(!hasPower && user.getPermissions() != null) {
for (Permission ga : user.getPermissions()) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(ga.getUri());
if (hasPower=matcher.matches(request)) {
break;
}
}
}
if(!hasPower) { //当前请求没有权限
throw new BadCredentialsException("当前请求没有权限,请联系管理员");
}
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//设置为已登录
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}catch (BadCredentialsException e){
log.error("拦截token验证", e);
// ExceptionPrinterUtil.instance().write(response, e.getMessage(), "UTF-8");
throw new BadCredentialsException(e.getMessage());
}
}
/**
* 解析jwt
* @param jsonWebToken
* @param
* @return
*/
public static Claims parseJWT(String jsonWebToken) {
try {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(BASE64_BIARY))
.parseClaimsJws(jsonWebToken).getBody();
return claims;
} catch (ExpiredJwtException eje) {
log.error("===== Token过期 =", eje);
throw eje;
} catch (Exception e){
log.error("= token解析异常 =====", e);
throw e;
}
}
token校验通过后,就可以访问具体接口,请求数据,并返给前端