feat: 完善token续命刷新机制

This commit is contained in:
fit2cloud-chenyw
2021-03-08 18:19:57 +08:00
parent f46912392c
commit 150b4c3982
34 changed files with 497 additions and 113 deletions

View File

@@ -5,9 +5,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.PropertySource;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableCaching
@SpringBootApplication(exclude = {
QuartzAutoConfiguration.class,
LdapAutoConfiguration.class

View File

@@ -14,7 +14,7 @@ public interface AuthApi {
@PostMapping("/login")
Object login(LoginDto loginDto);
Object login(LoginDto loginDto) throws Exception;
@PostMapping("/userInfo")
@@ -23,6 +23,9 @@ public interface AuthApi {
@GetMapping("/isLogin")
Boolean isLogin();
@PostMapping("/logout")
String logout();
@GetMapping("/test")
String test();

View File

@@ -2,6 +2,7 @@ package io.dataease.auth.config;
import io.dataease.auth.entity.JWTToken;
import io.dataease.auth.entity.SysUserEntity;
import io.dataease.auth.entity.TokenInfo;
import io.dataease.auth.service.AuthUserService;
import io.dataease.auth.util.JWTUtils;
import org.apache.shiro.authc.AuthenticationException;
@@ -34,9 +35,8 @@ public class F2CRealm extends AuthorizingRealm {
//验证资源权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JWTUtils.getUsername(principals.toString());
SysUserEntity user = authUserService.getUser(username);
Long userId = user.getUserId();
Long userId = JWTUtils.tokenInfoByToken(principals.toString()).getUserId();
//SysUserEntity user = authUserService.getUserById(userId);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
Set<String> role = authUserService.roles(userId).stream().collect(Collectors.toSet());
simpleAuthorizationInfo.addRoles(role);
@@ -50,12 +50,14 @@ public class F2CRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得username用于和数据库进行对比
String username = JWTUtils.getUsername(token);
TokenInfo tokenInfo = JWTUtils.tokenInfoByToken(token);
Long userId = tokenInfo.getUserId();
String username = tokenInfo.getUsername();
if (username == null) {
throw new AuthenticationException("token invalid");
}
SysUserEntity user = authUserService.getUser(username);
SysUserEntity user = authUserService.getUserById(userId);
if (user == null) {
throw new AuthenticationException("User didn't existed!");
}
@@ -66,7 +68,7 @@ public class F2CRealm extends AuthorizingRealm {
} catch (Exception e) {
e.printStackTrace();
}
if (! JWTUtils.verify(token, username, pass)) {
if (! JWTUtils.verify(token, tokenInfo, pass)) {
throw new AuthenticationException("Username or password error");
}
return new SimpleAuthenticationInfo(token, token, "f2cReam");

View File

@@ -0,0 +1,17 @@
package io.dataease.auth.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component
public class RsaProperties {
public static String privateKey;
@Value("${rsa.private_key}")
public void setPrivateKey(String privateKey) {
RsaProperties.privateKey = privateKey;
}
}

View File

@@ -22,6 +22,7 @@ public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager getManager(F2CRealm f2cRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
@@ -49,6 +50,7 @@ public class ShiroConfig {
filterMap.put("f2cPerms", new F2CPermissionsFilter());
//filterMap.put("f2cRoles", new F2CRolesFilter());
filterMap.put("jwt", new JWTFilter());
/*filterMap.put("jwt", jwtFilter);*/
filterMap.put("logout", new F2CLogoutFilter());
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl("/permissionMiss");

View File

@@ -0,0 +1,21 @@
package io.dataease.auth.entity;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
@Data
@Builder
public class TokenInfo implements Serializable {
private String username;
private Long userId;
private Long lastLoginTime;
public String format(){
return username + "," +userId;
}
}

View File

@@ -1,25 +1,31 @@
package io.dataease.auth.filter;
import io.dataease.auth.entity.JWTToken;
import io.dataease.auth.entity.SysUserEntity;
import io.dataease.auth.entity.TokenInfo;
import io.dataease.auth.service.AuthUserService;
import io.dataease.auth.util.JWTUtils;
import io.dataease.commons.utils.CommonBeanFactory;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
/*@Autowired
private AuthUserService authUserService;*/
/**
* 判断用户是否想要登入。
@@ -67,25 +73,22 @@ public class JWTFilter extends BasicHttpAuthenticationFilter {
return false;
}
private String refreshToken(ServletRequest request, ServletResponse response) {
private String refreshToken(ServletRequest request, ServletResponse response) throws Exception{
// 获取AccessToken(Shiro中getAuthzHeader方法已经实现)
String token = this.getAuthzHeader(request);
// 获取当前Token的帐号信息
String username = JWTUtils.getUsername(token);
String password = "123456";
try {
String newToken = JWTUtils.sign(username, password);
JWTToken jwtToken = new JWTToken(newToken);
this.getSubject(request, response).login(jwtToken);
// 设置响应的Header头新Token
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.addHeader("Access-Control-Expose-Headers", "Authorization");
httpServletResponse.setHeader("Authorization", newToken);
return newToken;
}catch (Exception e){
e.printStackTrace();
}
return null;
TokenInfo tokenInfo = JWTUtils.tokenInfoByToken(token);
AuthUserService authUserService = CommonBeanFactory.getBean(AuthUserService.class);
SysUserEntity user = authUserService.getUserById(tokenInfo.getUserId());
String password = user.getPassword();
String newToken = JWTUtils.sign(tokenInfo, password);
JWTToken jwtToken = new JWTToken(newToken);
this.getSubject(request, response).login(jwtToken);
// 设置响应的Header头新Token
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.addHeader("Access-Control-Expose-Headers", "RefreshAuthorization");
httpServletResponse.setHeader("RefreshAuthorization", newToken);
return newToken;
}
@@ -113,8 +116,10 @@ public class JWTFilter extends BasicHttpAuthenticationFilter {
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
httpServletResponse.addHeader("Access-Control-Expose-Headers", "authentication-status");
httpServletResponse.setHeader("authentication-status", "invalid");
httpServletResponse.setStatus(401);
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
}

View File

@@ -4,14 +4,18 @@ import io.dataease.auth.api.AuthApi;
import io.dataease.auth.api.dto.CurrentRoleDto;
import io.dataease.auth.api.dto.CurrentUserDto;
import io.dataease.auth.api.dto.LoginDto;
import io.dataease.auth.config.RsaProperties;
import io.dataease.auth.entity.SysUserEntity;
import io.dataease.auth.entity.TokenInfo;
import io.dataease.auth.service.AuthUserService;
import io.dataease.auth.util.JWTUtils;
import io.dataease.auth.util.RsaUtil;
import io.dataease.commons.utils.BeanUtils;
import io.dataease.commons.utils.CodingUtil;
import io.dataease.commons.utils.ServletUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@@ -26,36 +30,35 @@ public class AuthServer implements AuthApi {
@Override
public Object login(@RequestBody LoginDto loginDto) {
public Object login(@RequestBody LoginDto loginDto) throws Exception {
String username = loginDto.getUsername();
String password = loginDto.getPassword();
SysUserEntity user = authUserService.getUser(username);
SysUserEntity user = authUserService.getUserByName(username);
String realPwd = user.getPassword();
if (StringUtils.isEmpty(realPwd)){
if (ObjectUtils.isEmpty(user)){
throw new RuntimeException("没有该用户!");
}
/*String pwd = RsaUtil.decryptByPrivateKey(RsaProperties.privateKey, password);
String realPass = RsaUtil.decryptByPrivateKey(RsaProperties.privateKey, realPwd);
if (!StrUtil.equals(pwd, realPass)){
throw new RuntimeException("密码错误!");
}*/
if (!StringUtils.equals(realPwd, password)){
//私钥解密
String pwd = RsaUtil.decryptByPrivateKey(RsaProperties.privateKey, password);
//md5加密
pwd = CodingUtil.md5(pwd);
if (!StringUtils.equals(pwd, realPwd)){
throw new RuntimeException("密码错误!");
}
/*Map<String,Object> result = new HashMap<>();
result.put("token", JWTUtils.sign(username, realPwd));*/
String token = JWTUtils.sign(username, realPwd);
ServletUtils.setToken(token);
Map<String,Object> result = new HashMap<>();
TokenInfo tokenInfo = TokenInfo.builder().userId(user.getUserId()).username(username).lastLoginTime(System.currentTimeMillis()).build();
String token = JWTUtils.sign(tokenInfo, realPwd);
result.put("token", token);
ServletUtils.setToken(token);
return result;
}
@Override
public CurrentUserDto userInfo() {
String token = ServletUtils.getToken();
String username = JWTUtils.getUsername(token);
SysUserEntity user = authUserService.getUser(username);
Long userId = JWTUtils.tokenInfoByToken(token).getUserId();
SysUserEntity user = authUserService.getUserById(userId);
CurrentUserDto currentUserDto = BeanUtils.copyBean(new CurrentUserDto(), user);
List<CurrentRoleDto> currentRoleDtos = authUserService.roleInfos(user.getUserId());
List<String> permissions = authUserService.permissions(user.getUserId());
@@ -64,7 +67,7 @@ public class AuthServer implements AuthApi {
return currentUserDto;
}
@PostMapping("/logout")
@Override
public String logout(){
return "success";
}

View File

@@ -9,7 +9,9 @@ public interface AuthUserService {
SysUserEntity getUser(String username);
SysUserEntity getUserById(Long userId);
SysUserEntity getUserByName(String username);
List<String> roles(Long userId);

View File

@@ -5,6 +5,7 @@ import io.dataease.auth.entity.SysUserEntity;
import io.dataease.base.mapper.ext.AuthMapper;
import io.dataease.auth.service.AuthUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -15,14 +16,27 @@ import java.util.stream.Collectors;
@Service
public class AuthUserServiceImpl implements AuthUserService {
private final String USER_CACHE_NAME = "users_info";
@Resource
private AuthMapper authMapper;
/**
* 此处需被F2CRealm登录认证调用 也就是说每次请求都会被调用 所以最好加上缓存
* @param userId
* @return
*/
@Cacheable(value = USER_CACHE_NAME, key = "'user' + #userId" )
@Override
public SysUserEntity getUserById(Long userId){
return authMapper.findUser(userId);
}
@Override
public SysUserEntity getUser(String username){
return authMapper.findUser(username);
public SysUserEntity getUserByName(String username) {
return authMapper.findUserByName(username);
}
@Override
public List<String> roles(Long userId){
return authMapper.roleCodes(userId);

View File

@@ -30,10 +30,14 @@ public class ShiroServiceImpl implements ShiroService {
filterChainDefinitionMap.put("/v3/**","anon");
filterChainDefinitionMap.put("/static/**", "anon");
// filterChainDefinitionMap.put("/401", "anon");
// filterChainDefinitionMap.put("/404", "anon");
// 登陆
// filterChainDefinitionMap.put("/api/auth/logout", "anon");
filterChainDefinitionMap.put("/api/auth/login", "anon");
// 退出
//filterChainDefinitionMap.put("/logout", "anon");
// 放行未授权接口,重定向使用
filterChainDefinitionMap.put("/unauth", "anon");
filterChainDefinitionMap.put("/display/**", "anon");
@@ -43,7 +47,6 @@ public class ShiroServiceImpl implements ShiroService {
// 被挤下线
filterChainDefinitionMap.put("/downline", "anon");
// 放行 end ----------------------------------------------------------
filterChainDefinitionMap.put("/logout", "logout");
/*List<ExtPermissionBean> extPermissionBeans = extUserMapper.getPermissions();
@@ -53,7 +56,7 @@ public class ShiroServiceImpl implements ShiroService {
filterChainDefinitionMap.put(item.getPath(), "jwt," + f2cPerms);
});
*/
filterChainDefinitionMap.put("/api/auth/logout", "logout");
filterChainDefinitionMap.put("/**", "jwt");
return filterChainDefinitionMap;
}

View File

@@ -5,15 +5,19 @@ import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.dataease.auth.entity.TokenInfo;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
public class JWTUtils {
// 过期时间5分钟
private static final long EXPIRE_TIME = 5*60*1000;
// token过期时间5分钟 (过期会自动刷新续命 目的是避免一直都是同一个token )
private static final long EXPIRE_TIME = 1*60*1000;
// 登录间隔时间 超过这个时间强制重新登录
private static final long Login_Interval = 2*60*1000;
/**
@@ -22,10 +26,12 @@ public class JWTUtils {
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
public static boolean verify(String token, TokenInfo tokenInfo, String secret) {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.withClaim("lastLoginTime", tokenInfo.getLastLoginTime())
.withClaim("username", tokenInfo.getUsername())
.withClaim("userId", tokenInfo.getUserId())
.build();
verifier.verify(token);
return true;
@@ -35,18 +41,22 @@ public class JWTUtils {
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
e.printStackTrace();
return null;
public static TokenInfo tokenInfoByToken(String token) {
DecodedJWT jwt = JWT.decode(token);
String username = jwt.getClaim("username").asString();
Long userId = jwt.getClaim("userId").asLong();
Long lastLoginTime = jwt.getClaim("lastLoginTime").asLong();
if (StringUtils.isEmpty(username) || ObjectUtils.isEmpty(userId) || ObjectUtils.isEmpty(lastLoginTime)){
throw new RuntimeException("token格式错误");
}
TokenInfo tokenInfo = TokenInfo.builder().username(username).userId(userId).lastLoginTime(lastLoginTime).build();
return tokenInfo;
}
public static boolean needRefresh(String token){
Date exp = JWTUtils.getExp(token);
return new Date().getTime() >= exp.getTime();
@@ -64,17 +74,19 @@ public class JWTUtils {
/**
* 生成签名,5min后过期
* @param username 用户
* @param tokenInfo 用户信息
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
public static String sign(TokenInfo tokenInfo, String secret) {
try {
Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withClaim("lastLoginTime", tokenInfo.getLastLoginTime())
.withClaim("username", tokenInfo.getUsername())
.withClaim("userId", tokenInfo.getUserId())
.withClaim("exp", date)
.withExpiresAt(date)
.sign(algorithm);

View File

@@ -0,0 +1,29 @@
package io.dataease.auth.util;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
public class RsaUtil {
/**
* 私钥解密
*
* @param privateKeyText 私钥
* @param text 待解密的文本
* @return /
* @throws Exception /
*/
public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
}

View File

@@ -15,11 +15,12 @@ public interface AuthMapper {
List<String> roleCodes(@Param("userId") Long userId);
List<String> permissions(@Param("userId") Long userId);
SysUserEntity findUser(@Param("username") String username);
SysUserEntity findUser(@Param("userId") Long userId);
SysUserEntity findUserByName(@Param("username") String username);
List<CurrentRoleDto> roles(@Param("userId") Long userId);

View File

@@ -21,6 +21,10 @@
<select id="findUser" resultMap="baseMap">
select user_id, username,nick_name, dept_id, password, enabled,email, phone from sys_user where user_id = #{userId}
</select>
<select id="findUserByName" resultMap="baseMap">
select user_id, username,nick_name, dept_id, password, enabled,email, phone from sys_user where username = #{username}
</select>

View File

@@ -1,5 +1,6 @@
package io.dataease.commons.utils;
import io.dataease.auth.entity.TokenInfo;
import io.dataease.auth.util.JWTUtils;
import io.dataease.base.domain.SysUser;
import io.dataease.service.sys.SysUserService;
@@ -18,9 +19,9 @@ public class AuthUtils {
public static SysUser getUser(){
String token = ServletUtils.getToken();
String username = JWTUtils.getUsername(token);
TokenInfo tokenInfo = JWTUtils.tokenInfoByToken(token);
SysUser sysUser = new SysUser();
sysUser.setUsername(username);
sysUser.setUserId(tokenInfo.getUserId());
SysUser user = sysUserService.findOne(sysUser);
return user;
}

View File

@@ -6,6 +6,7 @@ import com.github.pagehelper.PageHelper;
import io.dataease.commons.utils.PageUtils;
import io.dataease.commons.utils.Pager;
import io.dataease.controller.sys.request.SysUserCreateRequest;
import io.dataease.controller.sys.request.SysUserPwdRequest;
import io.dataease.controller.sys.request.SysUserStateRequest;
import io.dataease.controller.sys.request.UserGridRequest;
import io.dataease.controller.sys.response.SysUserGridResponse;
@@ -55,4 +56,11 @@ public class SysUserController {
public void updateStatus(@RequestBody SysUserStateRequest request){
sysUserService.updateStatus(request);
}
@ApiOperation("更新用户密码")
@PostMapping("/updatePwd")
public void updatePwd(@RequestBody SysUserPwdRequest request){
sysUserService.updatePwd(request);
}
}

View File

@@ -0,0 +1,17 @@
package io.dataease.controller.sys.request;
import lombok.Data;
import java.io.Serializable;
@Data
public class SysUserPwdRequest implements Serializable {
private Long userId;
private String password;
private String repeatPassword;
private String newPassword;
}

View File

@@ -10,6 +10,7 @@ import io.dataease.base.mapper.ext.ExtSysUserMapper;
import io.dataease.commons.utils.BeanUtils;
import io.dataease.commons.utils.CodingUtil;
import io.dataease.controller.sys.request.SysUserCreateRequest;
import io.dataease.controller.sys.request.SysUserPwdRequest;
import io.dataease.controller.sys.request.SysUserStateRequest;
import io.dataease.controller.sys.request.UserGridRequest;
import io.dataease.controller.sys.response.SysUserGridResponse;
@@ -17,6 +18,7 @@ import io.dataease.controller.sys.response.SysUserRole;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@@ -26,6 +28,7 @@ import java.util.stream.Collectors;
@Service
public class SysUserService {
private final static String USER_CACHE_NAME = "users_info";
private final static String DEFAULT_PWD = "DataEase123..";
@Resource
@@ -83,6 +86,33 @@ public class SysUserService {
return sysUserMapper.updateByPrimaryKeySelective(sysUser);
}
/**
* 修改用户密码清楚缓存
* @param request
* @return
*/
@CacheEvict(value = USER_CACHE_NAME, key = "'user' + #request.userId")
public int updatePwd(SysUserPwdRequest request) {
if (!StringUtils.equals(request.getPassword(), request.getRepeatPassword())){
throw new RuntimeException("两次密码不一致");
}
SysUser temp = new SysUser();
temp.setUserId(request.getUserId());
SysUser user = findOne(temp);
if (ObjectUtils.isEmpty(user)) {
throw new RuntimeException("用户不存在");
}
if (!StringUtils.equals(request.getPassword(), user.getPassword())){
throw new RuntimeException("密码错误");
}
SysUser sysUser = new SysUser();
sysUser.setUserId(request.getUserId());
sysUser.setPassword(CodingUtil.md5(request.getNewPassword()));
return sysUserMapper.updateByPrimaryKeySelective(sysUser);
}
/**
* 删除用户角色关联
* @param userId
@@ -108,6 +138,7 @@ public class SysUserService {
});
}
@CacheEvict(value = USER_CACHE_NAME, key = "'user' + #userId")
@Transactional
public int delete(Long userId){
deleteUserRoles(userId);