文章目录
思考
-
为什么需要用token来做?传统的session为什么不可以?token有何优势。
-
session存在的问题 :①前后端分离项目,前端可能是web/app等,对于存储sessionId的cookie问题;②session存在CSRF跨站伪造请求攻击;③ 服务器压力增大,通常session存储在内存中,用户量大服务器压力也大;③ 服务器分布式部署情况下,session就会不一定获取的到,存在不在一台服务器中的情况,拓展性很差。
-
token有何优势 :token与session的不同主要:
①认证成功后,会对当前用户数据进行加密,生成一个加密字符串token,返还给客户端**(服务器端并不进行保存)**
②浏览器会将接收到的token值存储在Local Storage中,(通过js代码写入Local Storage,通过js获取,并不会像cookie一样自动携带)
③再次访问时服务器端对token值的处理:服务器对浏览器传来的token值进行解密,解密完成后进行用户数据的查询,如果查询成功,则通过认证,实现状态保持,所以,即时有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。
-
一、登录流程
① 用户输入用户名密码来通过数据库验证,这里不通过shiro(主体的login方法)登录,并把token创建的时间戳存储在redis中把AUTH_REFRESH_TOKEN + jwtId
作为redis的key。
② 由于第一次登录验证,需要将token从Controller
层返回给前端用于存储到localStorge
中。
③ 客户端访问服务器时请求头携带token,传递到后端shiro访问控制过滤器(AccessControlFilter)进行拦截使用shiro的login方法进行认证授权,在realm中查询角色权限授权
-
后续请求可通过继承shiro访问控制过滤器设置响应头带有token传递给客服端
-
token过期操作:在shiro(AccessControlFilter)中访问许可(isAccessAllowed)中处理jwt过期异常,如token一小时过期,这时在异常处理中上锁(防多刷),再去判断token自己的时间戳和redis之前存储的时间戳是否相等,再颁发新的token,设置新的响应头(给客服端刷新localStorge中的token),这时我们在redis中存储一个30s的即将过期的token,放多次刷新token,该用户token过期将从redis中取这个token进行授权就无需刷新token了
二、服务器代码实现
配置及工具类
yml
#custom config
config:
# JWT认证加密私钥
encrypt-secret: 6Dx8SIuaHXJYnpsG18SSpjPs50lZcT52
# AccessToken过期时间(秒)
access-token-expire-time: 600
# RefreshToken过期时间(秒)
refresh-token-expire-time: 3600
JWTToken(继承AuthenticationToken)
public class JWTToken implements AuthenticationToken {
/**
* 凭证
*/
private final String accessToken;
public JWTToken(String accessToken) {
this.accessToken = accessToken;
}
@Override
public Object getPrincipal() {
return accessToken;
}
@Override
public Object getCredentials() {
return accessToken;
}
JWTUtils
/**
* JWT加密密钥
*/
private static String ENCRYPT_SECRET;
/**
* AccessToken过期时间(秒)
*/
private static long ACCESS_TOKEN_EXPIRE_TIME;
/**
* 扩展字段 isAdmin
*/
private static final String EXTEND_IS_ADMIN = "isAdmin";
/**
* 扩展字典 时间戳
*/
private static final String EXTEND_TIMESTAMP = "timestamp";
/**
* 生成jwt
*
* @param jwtId jwt标识
* @param subjectId 主体id
* @param isAdmin 是否为管理员
* @param currentTimeMillis 签发时间
* @return jwt字符串
*/
public static String generate(String jwtId, String subjectId, boolean isAdmin, String currentTimeMillis) {
Algorithm algorithm = Algorithm.HMAC256(ENCRYPT_SECRET);
return JWT.create()
//扩展字段
.withClaim(EXTEND_IS_ADMIN, isAdmin)
.withClaim(EXTEND_TIMESTAMP, currentTimeMillis)
//jwt标识
.withJWTId(jwtId)
//主体id
.withSubject(subjectId)
//签发时间
.withIssuedAt(new Date(Long.parseLong(currentTimeMillis)))
//有效时间
.withExpiresAt(new Date(Long.parseLong(currentTimeMillis) + ACCESS_TOKEN_EXPIRE_TIME * 1000))
//签名
.sign(algorithm);
}
/**
* 验证jwt
*
* @param jwt jwt字符串
*/
public static void verify(String jwt) {
Algorithm algorithm = Algorithm.HMAC256(ENCRYPT_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwt);
}
/**
* 获取jwt标识
*
* @param jwt jwt字符串
* @return jwt标识
*/
public static String getId(String jwt) {
DecodedJWT decodedJWT = JWT.decode(jwt);
return decodedJWT.getId();
}
/**
* 获取主体id
*
* @param jwt jwt字符串
* @return 主体id
*/
public static String getSubjectId(String jwt) {
DecodedJWT decodedJWT = JWT.decode(jwt);
return decodedJWT.getSubject();
}
/**
* 是否为管理员
*
* @param jwt jwt字符串
* @return 是否为管理员
*/
public static boolean isAdmin(String jwt) {
return getClaim(jwt, EXTEND_IS_ADMIN).asBoolean();
}
/**
* 获取时间戳
*
* @param jwt jwt字符串
* @return 时间戳
*/
public static String getTimestamp(String jwt) {
return getClaim(jwt, EXTEND_TIMESTAMP).asString();
}
/**
* 解析jwt
*
* @param jwt jwt字符串
* @param claim 声明字段
* @return 字段内容
*/
public static Claim getClaim(String jwt, String claim) {
DecodedJWT decodedJWT = JWT.decode(jwt);
return decodedJWT.getClaim(claim);
}
@Value("${config.access-token-expire-time}")
public void setAccessTokenExpireTime(long accessTokenExpireTime) {
ACCESS_TOKEN_EXPIRE_TIME = accessTokenExpireTime;
}
@Value("${config.encrypt-secret}")
public void setEncryptSecret(String encryptSecret) {
ENCRYPT_SECRET = encryptSecret;
}
1. 验证用户名、密码
登录验证控制器
/**
* 账号密码登录
*
* @param loginName 账号/手机号
* @param password 密码
* @param captchaKey 图形验证码key
* @param captcha 图形验证码
*/
@PostMapping("/accountLogin")
public ResponseEntity accountLogin(@RequestParam String loginName, @RequestParam String password,
@RequestParam String captchaKey, @RequestParam String captcha,
HttpServletRequest request) {
if (StringUtils.isAnyBlank(loginName, password)) {
return ResponseEntity.error("用户名和密码不能为空.");
}
if (!CodeValidator.checkCaptcha(captchaKey, captcha)) {
return ResponseEntity.error("验证码错误.");
}
UserCredentialEntity credential = credentialService.getCredentialByLoginName(loginName);
if (credential == null) {
credential = credentialService.getCredentialByPhone(loginName);
}
if (credential != null) {
if (BCrypt.checkpw(password, credential.getPassword())) {
//更新登录信息
credentialService.updateLastLoginInfo(loginName, ServletUtil.getClientIP(request), new Date());
//签发accessToken
String accessToken = issueAccessToken(credential.getId(), false);
return ResponseEntity.ok(accessToken, "登录成功");
}
}
return ResponseEntity.error("登录失败,账号密码错误.");
颁发token
/**
* 颁发AccessToken
*
* @param subjectId 用户id
* @return accessToken
*/
private String issueAccessToken(String subjectId, boolean isAdmin) {
//jwt标识
String jwtId = UUID.randomUUID().toString(true);
//设置RefreshToken,时间戳为当前时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
RedisUtils.set(RedisKeys.AUTH_REFRESH_TOKEN + jwtId, currentTimeMillis, refreshTokenExpireTime);
//生成accessToken
return JWTUtils.generate(jwtId, subjectId, isAdmin, currentTimeMillis);
}
- 返回给客户端,客户端查看response返回的token存储localstorge,接着发送token请求
2. token的验证认证和过期情况
shiroConfig
@Configuration
public class ShiroConfig {
/**
* 安全过滤器
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
// 添加认证过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 配置拦截规则
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//无需登录即可访问的url
//登录
filterChainDefinitionMap.put("/accountLogin", "anon");
filterChainDefinitionMap.put("/smsLogin", "anon");
filterChainDefinitionMap.put("/alogin", "anon");
//注销
filterChainDefinitionMap.put("/logout", "anon");
//图形化验证码
filterChainDefinitionMap.put("/captcha", "anon");
//短信验证码
filterChainDefinitionMap.put("/vcode", "anon");
//上传文件下载
filterChainDefinitionMap.put("/filestore/**", "anon");
//网站门户(包含注册、找回密码等url)
filterChainDefinitionMap.put("/portal/**", "anon");
//支付回调
filterChainDefinitionMap.put("/alipay/callback", "anon");
filterChainDefinitionMap.put("/alipay/notify", "anon");
//静态html页面
filterChainDefinitionMap.put("/**/*.html", "anon");
//其余url需要进行accessToken验证
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 安全管理
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//多种方式认证
securityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> realms = new ArrayList<>();
realms.add(jwtRealm());
securityManager.setRealms(realms);
return securityManager;
}
/**
* 多方式认证
*/
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
/**
* token认证
*/
@Bean
public JWTRealm jwtRealm() {
JWTRealm jwtRealm = new JWTRealm();
jwtRealm.setCredentialsMatcher(new AllowAllCredentialsMatcher());
return jwtRealm;
}
/**
* 开启注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/**
* setUsePrefix(true)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setUsePrefix(true);
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
JWTFilter
public class JWTFilter extends AccessControlFilter {
/**
* RefreshToken过期时间(秒)
*/
@Value("${config.refresh-token-expire-time}")
private long refreshTokenExpireTime;
/**
* accessToken key
*/
private final String ACCESS_TOKEN_KEY = "Authorization";
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String accessToken = getAccessToken(request);
if (StringUtils.isBlank(accessToken)) {
return false;
}
try {
getSubject(request, response).login(new JWTToken(accessToken));
} catch (Exception ex) {
Console.log(ex.getMessage());
//如果token过期则尝试刷新token
Throwable throwable = ex.getCause();
if (throwable instanceof TokenExpiredException) {
return this.refreshToken(accessToken, request, response);
}
return false;
}
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
ObjectMapper mapper = new ObjectMapper();
String responseBody = mapper.writeValueAsString(ResponseEntity.error(ErrorCode.AUTH_FAILED));
ServletUtil.write(httpResponse, responseBody, "application/json; charset=utf-8");
return false;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
httpServletResponse.setHeader("Access-Control-Expose-Headers", ACCESS_TOKEN_KEY);
httpServletResponse.setHeader("Cache-Control", "no-store");
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 刷新token,该方法为同步方法同一accessToken在同一时间只能调用一次,防止重复刷新token
*
* @return 是否刷新成功
*/
private boolean refreshToken(String accessToken, ServletRequest request, ServletResponse response) {
//加锁,保障同一token在同一时间只能刷新一次,避免重复刷新
String lockKey = LockKeys.LOCK_REFRESH_TOKEN_PREFIX + accessToken;
try {
LockUtils.lock(lockKey);
if (RedisUtils.hasKey(RedisKeys.AUTH_EXPIRING_TOKEN + accessToken)) {
String newToken = (String) RedisUtils.get(RedisKeys.AUTH_EXPIRING_TOKEN + accessToken);
// 提交给Realm进行认证
try {
this.getSubject(request, response).login(new JWTToken(newToken));
} catch (Exception ex) {
Console.log(ex.getMessage());
return false;
}
return true;
} else {
//获取用户id
String subjectId = JWTUtils.getSubjectId(accessToken);
String timestamp = JWTUtils.getTimestamp(accessToken);
String jwtId = JWTUtils.getId(accessToken);
boolean isAdmin = JWTUtils.isAdmin(accessToken);
if (RedisUtils.hasKey(RedisKeys.AUTH_REFRESH_TOKEN + jwtId)) {
String currentTimeMillisRedis = (String) RedisUtils.get(RedisKeys.AUTH_REFRESH_TOKEN + jwtId);
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (StringUtils.equals(currentTimeMillisRedis, timestamp)) {
// 生成新的jwt标识
String newJwtId = UUID.randomUUID().toString(true);
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 设置RefreshToken中的时间戳为当前最新时间戳并重置过期时间
RedisUtils.set(RedisKeys.AUTH_REFRESH_TOKEN + newJwtId, currentTimeMillis, refreshTokenExpireTime);
// 刷新AccessToken,设置时间戳为当前最新时间戳
String newAccessToken = JWTUtils.generate(newJwtId, subjectId, isAdmin, currentTimeMillis);
// 提交给Realm进行认证
try {
this.getSubject(request, response).login(new JWTToken(newAccessToken));
} catch (Exception ex) {
Console.log(ex.getMessage());
return false;
}
// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader(ACCESS_TOKEN_KEY, newAccessToken);
// 设置旧token设置为即将过期,30秒
RedisUtils.set(RedisKeys.AUTH_EXPIRING_TOKEN + accessToken, newAccessToken, 30);
// 删除旧的refresh token
RedisUtils.del(RedisKeys.AUTH_REFRESH_TOKEN + jwtId);
return true;
}
}
}
} finally {
LockUtils.unlock(lockKey);
}
return false;
}
/**
* 获取AccessToken
*
* @param request request
* @return AccessToken
*/
private String getAccessToken(ServletRequest request) {
HttpServletRequest httpRequest = WebUtils.toHttp(request);
String accessToken = httpRequest.getHeader(ACCESS_TOKEN_KEY);
if (StringUtils.isBlank(accessToken)) {
accessToken = httpRequest.getParameter(ACCESS_TOKEN_KEY);
}
return accessToken;
}
}
JWTRealm
public class JWTRealm extends AuthorizingRealm {
@Autowired
@Lazy
private AdministratorService administratorService;
@Autowired
@Lazy
private UserSubjectService subjectService;
@Autowired
@Lazy
private UserCredentialService credentialService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Object principal = principals.getPrimaryPrincipal();
//数据库查询角色进行授权
return new SimpleAuthorizationInfo(obtainRoles(principal));
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String accessToken = (String) authenticationToken.getCredentials();
//解析token
String uid;
String timestamp;
String jwtId;
boolean isAdmin;
try {
//验证jwt有效性
JWTUtils.verify(accessToken);
uid = JWTUtils.getSubjectId(accessToken);
timestamp = JWTUtils.getTimestamp(accessToken);
jwtId = JWTUtils.getId(accessToken);
isAdmin = JWTUtils.isAdmin(accessToken);
} catch (Exception ex) {
throw new AuthenticationException("token is invalid.", ex);
}
//验证时间戳
String currentTimeMillisRedis = (String) RedisUtils.get(RedisKeys.AUTH_REFRESH_TOKEN + jwtId);
if (!StringUtils.equals(timestamp, currentTimeMillisRedis)) {
throw new AuthenticationException("token timestamp incorrect.");
}
//运维管理员登录
if (isAdmin) {
AdministratorEntity admin = administratorService.getAdministrator(uid);
if (admin == null) {
throw new AuthenticationException("admin not found.");
}
//管理员context
AdminContext acontext = AdminContext.builder()
.accessToken(accessToken)
.adminId(admin.getId())
.name(admin.getName())
.loginName(admin.getLoginName())
.phone(admin.getPhone())
.email(admin.getEmail())
.build();
acontext.setRoles(obtainRoles(acontext));
return new SimpleAuthenticationInfo(acontext, accessToken, getName());
//参数:1.主体 2. 凭证 3. realm名字
}
}
/**
* 获取角色
*
* @param principal 登录人
* @return 角色列表
*/
private Set<String> obtainRoles(Object principal) {
Set<String> roles = new HashSet<>();
//TODO 添加角色
return roles;
}