会话流程如下:
- 用户输入账户密码,使用jsencrypt库通过RSA公钥加密密码后登录。
- 校验合法性,使用SimpleAsymmetricStringEncryptor(jasypt-spring-boot-starter)解密密码。
- 根据用户名查询用户信息,使用PasswordEncoder(spring-security-crypto)校验PasswordEncoder加密的密码(相同密码每次加密结果不一样)。
- 根据用户ID、浏览器信息、IP、SecureRandom随机数,通过RSA加密生成tokenID.
- 查询用户权限信息(权限变化时更新缓存)和用户基本信息存入Ehcache缓存。
- HttpServletResponse添加tokenId的header。
- 前端收到后将tokenId存在localStorage里,每次请求加载header里。后端过滤器过滤无效会话。
主要步骤如下
一、前端RSA加密函数
1、引入库
npm i jsencrypt -S
2、使用
import JsEncrypt from 'jsencrypt';
const publicKey = '-----BEGIN PUBLIC KEY-----\n' +
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtcfuLEDt5+lbx1jNFQCdT0iin\n' +
'cYwCHEr9s8ptXR6Ip2HKwB1CKLDcsMVzFtu3+OElYDcuxlJ1Bc6HdlwN7ZA35/Qv\n' +
'AWA49hj3oLM+/n37tNFqq60sTgfoEJNDWaBb9BvMSrwnd++d+knz0yLaHtct1t+V\n' +
'Yz8cE1E0F3n4unex1QIDAQAB\n' +
'-----END PUBLIC KEY-----';
const jsEncrypt = new JsEncrypt();
jsEncrypt.setPublicKey(publicKey);
export const encryptRSA = (password) => {
return jsEncrypt.encrypt(password);
};
二、后端RSA加密配置
1、生成RSA密钥对
linux下执行:
openssl genrsa -out rsa_1024_priv.pem 1024
openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem
openssl pkcs8 -topk8 -inform PEM -in rsa_1024_priv.pem -outform pem -nocrypt -out pkcs8_rsa_1024_priv.pem
放在如下:
2、引入依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
jasypt.encryptor.private-key-format=pem
jasypt.encryptor.private-key-location=RSA/rsa_1024_priv.pem
3、配置RSA加解密Bean
@Configuration
public class RASConfig {
@Bean
public SimpleAsymmetricStringEncryptor simpleAsymmetricStringEncryptor(){
SimpleAsymmetricConfig config = new SimpleAsymmetricConfig();
config.setPrivateKeyLocation("RSA/pkcs8_rsa_1024_priv.pem");
config.setPrivateKeyFormat(AsymmetricCryptography.KeyFormat.PEM);
config.setPublicKeyLocation("RSA/rsa_1024_pub.pem");
config.setPublicKeyFormat(AsymmetricCryptography.KeyFormat.PEM);
SimpleAsymmetricStringEncryptor encryptor = new SimpleAsymmetricStringEncryptor(config);
return encryptor;
}
}
4、使用示例:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EncryptTest {
@Autowired
private SimpleAsymmetricStringEncryptor stringEncryptor;
@Test
public void encryptTest() {
String encrypt = stringEncryptor.encrypt("admin");
String decrypt = stringEncryptor.decrypt(encrypt);
System.out.println("decrypt:");
System.out.println(decrypt);
System.out.println("encrypt:");
System.out.println(encrypt);
}
}
三、PasswordEncoder密码密文存储
1、引入依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
2、配置Bean
@Configuration
public class RASConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3、使用示例
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EncryptTest {
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void passwordEncoderTest() {
String password = "admin";
for (int i = 0; i < 10; i++) {
//每个计算出的Hash值都不一样
String hashPass = passwordEncoder.encode(password);
System.out.println(hashPass);
//虽然每次计算的密码Hash值不一样但是校验是通过的
boolean f = passwordEncoder.matches(password, hashPass);
System.out.println(f);
}
}
}
四、配置Ehcache
1、配置
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:cache/ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">
<!--timeToIdleSeconds 当缓存闲置n秒后销毁 -->
<!--timeToLiveSeconds 当缓存存活n秒后销毁 -->
<!-- 缓存配置
name:缓存名称。
maxElementsInMemory:缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk
store persists between restarts of the Virtual Machine. The default value
is false.
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是
LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。 -->
<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir/ehcache" />
<!-- 默认缓存 -->
<defaultCache maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap" />
</defaultCache>
<!-- Token -->
<cache name="TokenCache"
eternal="false"
maxElementsInMemory="10000"
maxEntriesLocalDisk="0"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="true"
maxEntriesLocalHeap="10000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</cache >
</ehcache>
二、使用
package com.my.world.securitymanagement.api.token;
import com.my.world.common.rest.utils.RequestContextUtil;
import com.my.world.securitymanagement.api.po.User;
import com.my.world.securitymanagement.api.vo.Token;
import com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor;
import lombok.extern.slf4j.Slf4j;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
/**
* @program: MyWorld
* @description: ehcache会话服务
* @author: xue chi
* @create: 2019-12-12 16:55
**/
@Service
@Slf4j
public class EhTokenManager implements ITokenManager {
public static final int Expiry = 600;
@Autowired
private CacheManager cacheManager;
private Cache tokenCache;
@Autowired
private SimpleAsymmetricStringEncryptor stringEncrypt;
private static SecureRandom secureRandom;
static {
try {
secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
e.printStackTrace();
}
}
@Override
public String createToken(User user) {
Cache tokenCache = getTokenCache();
HttpServletRequest request = RequestContextUtil.getRequest();
String osAndBrowserInfo = RequestContextUtil.getOsAndBrowserInfo();
String remoteHost = RequestContextUtil.getRemoteHost();
String tokenId = request.getHeader(ITokenManager.TOKEN);
if (checkValidTokenId(tokenId)) {
Element element = tokenCache.get(tokenId);
if (element != null) {
return tokenId;
}
}
int random = secureRandom.nextInt();
tokenId = stringEncrypt.encrypt(user.getId() + "_" + osAndBrowserInfo + "_" + remoteHost + "_" + random);
Token token = new Token();
token.setTokenId(tokenId);
token.setUser(user);
Element element = new Element(tokenId, token, Expiry, 0);
tokenCache.put(element);
HttpServletResponse response = RequestContextUtil.getResponse();
response.setHeader(ITokenManager.TOKEN, tokenId);
return tokenId;
}
@Override
public boolean checkValidTokenId(String tokenId) {
if (StringUtils.isEmpty(tokenId)) {
return false;
}
String decrypt = null;
try {
decrypt = stringEncrypt.decrypt(tokenId);
} catch (Exception e) {
log.error("解析token失败", e);
return false;
}
String[] s = decrypt.split("_");
if (s.length < 4) {
return false;
}
String userId = s[0];
String browner = s[1];
String ip = s[2];
String osAndBrowserInfo = RequestContextUtil.getOsAndBrowserInfo();
String remoteHost = RequestContextUtil.getRemoteHost();
if (osAndBrowserInfo.equals(browner) && remoteHost.equals(ip)) {
Token token = getToken(tokenId);
return token != null;
}
return false;
}
@Override
public void loginOff(String tokenId) {
Cache tokenCache = getTokenCache();
tokenCache.remove(tokenId);
}
@Override
public Token getToken(String tokenId) {
if (StringUtils.isEmpty(tokenId)) {
return null;
}
Cache tokenCache = getTokenCache();
Element element = tokenCache.get(tokenId);
if (element == null) {
return null;
}
Object objectValue = element.getObjectValue();
if (objectValue instanceof Token) {
return (Token) objectValue;
}
return null;
}
@Override
public Token getToken() {
HttpServletRequest request = RequestContextUtil.getRequest();
String tokenId = request.getHeader(ITokenManager.TOKEN);
return getToken(tokenId);
}
public Cache getTokenCache() {
if (this.tokenCache == null) {
tokenCache = cacheManager.getCache("TokenCache");
}
return tokenCache;
}
}
五、过滤器
package com.my.world.securitymanagement.api.filter;
import com.my.world.common.rest.exception.UserNotLoginException;
import com.my.world.common.rest.utils.JsonUtil;
import com.my.world.securitymanagement.api.token.ITokenManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @program: MyWorld
* @description: 登录过滤器
* @author: xue chi
* @create: 2019-12-13 14:30
**/
@Configuration
@WebFilter(filterName = "loginFilter")
public class LoginFilter implements Filter {
@Autowired
private ITokenManager iTokenManager;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String tokenId = httpRequest.getHeader(ITokenManager.TOKEN);
String requestURI = ((HttpServletRequest) request).getRequestURI();
if ("/rest/login".equals(requestURI)) {
chain.doFilter(request, response);
return;
}
boolean valid = iTokenManager.checkValidTokenId(tokenId);
if (!valid) {
httpResponse.setContentType("text/html;charset=utf8");
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter writer = httpResponse.getWriter();
String json = JsonUtil.object2Json(new UserNotLoginException("请重新登录!"));
writer.write(json);
writer.flush();
writer.close();
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
完
如与你心中完美的方案不同,请留下你的意见-。-