會(huì)話流程如下:
- 用戶輸入賬戶密碼,使用jsencrypt庫(kù)通過(guò)RSA公鑰加密密碼后登錄庄呈。
- 校驗(yàn)合法性谜慌,使用SimpleAsymmetricStringEncryptor(jasypt-spring-boot-starter)解密密碼僧叉。
- 根據(jù)用戶名查詢用戶信息郊楣,使用PasswordEncoder(spring-security-crypto)校驗(yàn)PasswordEncoder加密的密碼(相同密碼每次加密結(jié)果不一樣)骤竹。
- 根據(jù)用戶ID跟畅、瀏覽器信息凭舶、IP晌块、SecureRandom隨機(jī)數(shù),通過(guò)RSA加密生成tokenID.
- 查詢用戶權(quán)限信息(權(quán)限變化時(shí)更新緩存)和用戶基本信息存入Ehcache緩存帅霜。
- HttpServletResponse添加tokenId的header匆背。
- 前端收到后將tokenId存在localStorage里,每次請(qǐng)求加載header里身冀。后端過(guò)濾器過(guò)濾無(wú)效會(huì)話钝尸。
主要步驟如下
一、前端RSA加密函數(shù)
1搂根、引入庫(kù)
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密鑰對(duì)
linux下執(zhí)行:
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
放在如下:
rsa.png
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密碼密文存儲(chǔ)
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++) {
//每個(gè)計(jì)算出的Hash值都不一樣
String hashPass = passwordEncoder.encode(password);
System.out.println(hashPass);
//雖然每次計(jì)算的密碼Hash值不一樣但是校驗(yàn)是通過(guò)的
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 當(dāng)緩存閑置n秒后銷(xiāo)毀 -->
<!--timeToLiveSeconds 當(dāng)緩存存活n秒后銷(xiāo)毀 -->
<!-- 緩存配置
name:緩存名稱背蟆。
maxElementsInMemory:緩存最大個(gè)數(shù)。
eternal:對(duì)象是否永久有效哮幢,一但設(shè)置了带膀,timeout將不起作用。
timeToIdleSeconds:設(shè)置對(duì)象在失效前的允許閑置時(shí)間(單位:秒)橙垢。僅當(dāng)eternal=false對(duì)象不是永久有效時(shí)使用本砰,可選屬性,默認(rèn)值是0钢悲,也就是可閑置時(shí)間無(wú)窮大点额。
timeToLiveSeconds:設(shè)置對(duì)象在失效前允許存活時(shí)間(單位:秒)。最大時(shí)間介于創(chuàng)建時(shí)間和失效時(shí)間之間莺琳。僅當(dāng)eternal=false對(duì)象不是永久有效時(shí)使用还棱,默認(rèn)是0.,也就是對(duì)象存活時(shí)間無(wú)窮大惭等。
overflowToDisk:當(dāng)內(nèi)存中對(duì)象數(shù)量達(dá)到maxElementsInMemory時(shí)珍手,Ehcache將會(huì)對(duì)象寫(xiě)到磁盤(pán)中。 diskSpoolBufferSizeMB:這個(gè)參數(shù)設(shè)置DiskStore(磁盤(pán)緩存)的緩存區(qū)大小辞做。默認(rèn)是30MB琳要。每個(gè)Cache都應(yīng)該有自己的一個(gè)緩沖區(qū)。
maxElementsOnDisk:硬盤(pán)最大緩存?zhèn)€數(shù)秤茅。
diskPersistent:是否緩存虛擬機(jī)重啟期數(shù)據(jù) Whether the disk
store persists between restarts of the Virtual Machine. The default value
is false.
diskExpiryThreadIntervalSeconds:磁盤(pán)失效線程運(yùn)行時(shí)間間隔稚补,默認(rèn)是120秒。 memoryStoreEvictionPolicy:當(dāng)達(dá)到maxElementsInMemory限制時(shí)框喳,Ehcache將會(huì)根據(jù)指定的策略去清理內(nèi)存课幕。默認(rèn)策略是
LRU(最近最少使用)。你可以設(shè)置為FIFO(先進(jìn)先出)或是LFU(較少使用)五垮。
clearOnFlush:內(nèi)存數(shù)量最大時(shí)是否清除乍惊。 -->
<!-- 磁盤(pán)緩存位置 -->
<diskStore path="java.io.tmpdir/ehcache" />
<!-- 默認(rèn)緩存 -->
<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會(huì)話服務(wù)
* @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;
}
}
五放仗、過(guò)濾器
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: 登錄過(guò)濾器
* @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("請(qǐng)重新登錄润绎!"));
writer.write(json);
writer.flush();
writer.close();
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
完
如與你心中完美的方案不同,請(qǐng)留下你的意見(jiàn)-诞挨。-