token的意思,即"令牌",有这个令牌就可以进行访问,就具有一定的权限,在传统的应用中,一般是存储于session,但在当下很多分布式微服务的应用中,session就显得力不从心了。当用户第一次登陆之后,服务端生成一个token并返回给客户端,客户端每次以后带着这个token访问即可,无需用户名和密码。token可以防止表单重复提交和身份验证等用途
流程:
1、用户登录之后,先校验用户名和密码
2、用户名和密码通过之后生成token,以zset加分数存储redis,方便后期排行进行删除不活跃的用户
3、将token返回前端,后面的每次请求携带token
4、验证token,通过则返回相应的数据,删除token,再重新生成一个token返回给前端存储在本地。不通过,重新登录
也要进行限制token无限增加,比如登录之后生成token之后,无限登录,导致redis中的token无限增加以及相关用户信息修改之后要进行重新生成token
application.yml:看redis配置就好,这里是单机
server:
port: 8081
# 下面是配置undertow作为服务器的参数
undertow:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io-threads: 4
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker-threads: 20
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 1024
# 是否分配的直接内存
direct-buffers: true
#启用shutdown
#endpoints:
# shutdown:
# enable: true
#禁用密码验证
#endpoints:
#shutdown:
#sensitive: false
#linux关闭的脚本
#curl -X POST host:port/shutdown
#开启shutdown的安全验证
#endpoints:
#shutdown:
#sensitive: true
#验证用户名和密码
#security:
#user:
#name: admin
#password: admin
#角色
#management:
#address: 127.0.0.1
#port: 8081
#security:
#role: SUPERUSER
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
platform: mysql
url: jdbc:mysql://xxx.xxx.xxx.xx:5306/miniprogram?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: root
password: admin
# ELASTICSEARCH (ElasticsearchProperties)
# Elasticsearch cluster name.
# data:
# elasticsearch:
# cluster-name: elasticsearch
# cluster-nodes: 192.168.13.111:9300,192.168.13.222:9300
# repositories:
# enabled: true
redis:
# database: 1
host: 127.0.0.1
port: 6379
password:
timeout: 10000
lettuce:
pool:
minIdle: 0
maxIdle: 10
maxWait: 10000
max-active: 10
# cluster:
# nodes:
# - 192.168.91.5:9001
# - 192.168.91.5:9002
# - 192.168.91.5:9003
# - 192.168.91.5:9004
# - 192.168.91.5:9005
# - 192.168.91.5:9006
activemq:
queueName: mvp.queue
topicName: mvp.topic
#账号密码
user: user
password: user
#URL of the ActiveMQ broker.
broker-url: tcp://localhost:61616
in-memory: false
#必须使用连接池
pool:
#启用连接池
enabled: true
#连接池最大连接数
max-connections: 5
#空闲的连接过期时间,默认为30秒
idle-timeout: 30s
mybatis:
typeAliasesPackage: com.pinyu.miniprogram.mysql.entity
mapper-locations: classpath:mapper/**/*Mapper.xml
mapper:
mappers: com.pinyu.miniprogram.mysql.mappers.BaseMapper
identity: mysql
#logging.config:
# classpath: test/log4j2_test.xml
自定义token注解,只要在controller方法贴上此注解表示此请求是必须要进行身份验证
package com.pinyu.miniprogram.global.ann;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireToken {
}
登录:
@RequestMapping("/login")
public String login(@RequestBody Map logMap) throws Exception {
String memberName = logMap.get("memberName");
String pwd = logMap.get("pwd");
MemberEntity m = new MemberEntity();
m.setMemberName(memberName);
pwd = MD5Utils.convertMD5(MD5Utils.md5(pwd));
m.setPwd(pwd);
List list = service.select(m);
if (list != null && list.size() > 0) {
MemberEntity member = list.get(0);
token.delToken(member);//清除之前redis有的token信息
String memberToken = token.saveToken(member);//重新生成token
return JsonMsg.OK(memberToken);
} else {
return JsonMsg.OK("用户名或密码错误");
}
}
Token:
package com.pinyu.miniprogram.utils;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.pinyu.miniprogram.config.redis.RedisUtils;
import com.pinyu.miniprogram.mysql.entity.member.MemberEntity;
import com.pinyu.miniprogram.utils.date.DateUtils;
import com.pinyu.miniprogram.utils.result.Code;
import com.pinyu.miniprogram.utils.result.JsonMsg;
import com.pinyu.miniprogram.utils.rsa.RSAUtils;
@Component
public class Token {
private final static String TOKEN_ERROR = "error";
public final static Integer TOKEN_ERROR_CODE = -1;
public static final String TOKEN_VALIDA_NULL="token为空";
public static final String TOKEN_VALIDA_ERROR="token验证错误";
public static final String TOKEN_VALIDA_FAIL="token验证失败";
public static String Error(String msg) {
Map map = new HashMap();
map.put("code", TOKEN_ERROR_CODE);
map.put("msg", msg);
return JSONObject.toJSONStringWithDateFormat(map, DateUtils.YYYY_MM_DD_HH_MM_SS,
SerializerFeature.WriteMapNullValue);
}
public static final String MEMBER_TOKEN = "MEMBER_TOKEN";
@Autowired
private RedisUtils redisUtils;
// 生成token前的格式为token:id:时间:六位随机数
public String generateToken(Long id) throws Exception{
StringBuilder tokenBuilder = new StringBuilder();
//生成未加密的token:
tokenBuilder.append("token:")
.append(id).append(":")
.append(new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())+":")
.append(new Random().nextInt((999999 - 111111 + 1)) + 111111);
String token=(RSAUtils.privateEncrypt(tokenBuilder.toString(), RSAUtils.getPrivateKey(RSAUtils.privateKey)));
System.out.println("token=>" + token.toString());
return token.toString();
}
public String saveToken(MemberEntity member) throws Exception {
String token = generateToken(member.getId());
redisUtils.zsetAdd(MEMBER_TOKEN, token, Double.valueOf(System.currentTimeMillis()));// 设置zset用于线程根据分数定时清理不活跃用户
redisUtils.set(token, member, 60 * 60 * 24 * 30L);// 存储相关用户信息(权限等信息)
redisUtils.set(String.valueOf(member.getId()), token);// 用于重新登录,但之前token还存在的情况,通过id获取相应的token来进行之前的token清理
return token;
}
public void delToken(MemberEntity member) {
Object object = redisUtils.get(String.valueOf(member.getId()));
if (object != null) {
String token = (String) object;
redisUtils.del(token);// 移除token以及相对应的权限信息
redisUtils.del(String.valueOf(member.getId()));// 移除token
redisUtils.remove(MEMBER_TOKEN, token);// 移除zset排行的token,避免一个用户重复排行
}
}
}
RedisUtils:
package com.pinyu.miniprogram.config.redis;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/**
* @author ypp 创建时间:2018年11月19日 下午4:58:04
* @Description: TODO(redis缓存工具类)
*/
@Component
public class RedisUtils {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param by 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param by 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map
RedisConfiguration:
package com.pinyu.miniprogram.config.redis;
import java.lang.reflect.Method;
import java.time.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author ypp 创建时间:2018年12月27日 下午2:55:17
* @Description: TODO(配置redis)
*/
@Configuration
@EnableCaching // spring中注解驱动的缓存管理功能
public class RedisConfiguration extends CachingConfigurerSupport {
private Logger logger = LogManager.getLogger(RedisConfiguration.class);
@Bean
public KeyGenerator KeyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
// @Bean
// CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 初始化一个RedisCacheWriter
// RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
// 设置CacheManager的值序列化方式为JdkSerializationRedisSerializer,但其实RedisCacheConfiguration默认就是使用StringRedisSerializer序列化key,JdkSerializationRedisSerializer序列化value,所以以下注释代码为默认实现
// ClassLoader loader = this.getClass().getClassLoader();
// JdkSerializationRedisSerializer jdkSerializer = new
// JdkSerializationRedisSerializer(loader);
// RedisSerializationContext.SerializationPair pair =
// RedisSerializationContext.SerializationPair.fromSerializer(jdkSerializer);
// RedisCacheConfiguration
// defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
// RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
// 设置默认超过期时间是30秒
// defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
// 初始化RedisCacheManager
// RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
// return cacheManager;
// }
// SpringBoot2.0之后,spring容器是自动的生成了StringRedisTemplate和RedisTemplate,可以直接注入
// 新增配置类RedisTemplate
@Bean("redisTemplate")
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// ObjectMapper objectMapper = new ObjectMapper();
// objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
RedisSerializer redisSerializer = new StringRedisSerializer();
// key
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
// value
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 缓存配置对象
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofMinutes(30L)) // 设置缓存的默认超时时间:30分钟
.disableCachingNullValues() // 如果是空值,不缓存
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) // 设置key序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((valueSerializer()))); // 设置value序列化器
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
// 使用Jackson序列化器
private RedisSerializer valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
private RedisSerializer keySerializer() {
return new StringRedisSerializer();
}
}
key的token存储member用户信息,key为id的对应token标识,这里就比较简单,只有了id,没有其他操作,单独存储一个token用于使用token查找member并更新member用户信息,存储zset主要用于利用毫秒分数定时清理不活跃的用户,比如清除排行5W后的用户信息,允许redis最多同时允许登录5W个用户,这里可以根据redis服务器配置进行处理
rsa加密token信息,防止token泄露导致用户信息/id被泄露,rsa非对称加密,只有自己的秘钥才可以解密,自己生成一对秘钥就好
RSAUtils:
package com.pinyu.miniprogram.utils.rsa;
import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
public class RSAUtils {
public static String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALVgisuDj0LMDf5i89wC2SvSujBYj5vshKFBAz3OfKvSmSXgVteN1dH6NRcyi5K6wNrVXu-4KsUm4Uf37rAiQvsCAwEAAQ";
public static String privateKey = "MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAtWCKy4OPQswN_mLz3ALZK9K6MFiPm-yEoUEDPc58q9KZJeBW143V0fo1FzKLkrrA2tVe77gqxSbhR_fusCJC-wIDAQABAkBbws_1TkW4QYwC2wUMldRRO3c-5k8hT3N6MW32YvTn5_XBWRwMjrl-t2G1kli1TIyrv8U2MiNiV5rm3KAgMRqBAiEA3OOp8y-gTaNzr9pHPYS7NEZJktwhDdabjBF9U7qbnxECIQDSNRCDq40ArmE1fMGvpt2nYrIGRzveW0PLksPPFeIZSwIgCa_KMinyg7UZS6rs2NvLQd2bOF-C65Jvu9LAhj12uaECIA_C7tQQnuf4K03JZvR2vJP6cILMAI8xpKm0_X2flG51AiB-7mj7JRaDoSHEYRfaCiCFY-js-iLH2sFBBdFo63wttw";
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA";
public static Map createKeys(int keySize) {
// 为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
// 初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
// 生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
// 得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
// 得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
Map keyPairMap = new HashMap();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
return keyPairMap;
}
/**
* 得到公钥
*
* @param publicKey
* 密钥字符串(经过base64编码)
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key;
}
/**
* 得到私钥
*
* @param privateKey
* 密钥字符串(经过base64编码)
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key;
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET),
publicKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
*/
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data),
privateKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥加密
*
* @param data
* @param privateKey
* @return
*/
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET),
privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 公钥解密
*
* @param data
* @param publicKey
* @return
*/
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data),
publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
int maxBlock = 0;
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
// Map keyMap = RSAUtils.createKeys(512);
// String publicKey = keyMap.get("publicKey");
// String privateKey = keyMap.get("privateKey");
// System.out.println("公钥: \n\r" + publicKey);
// System.out.println("私钥: \n\r" + privateKey);
// System.out.println("公钥加密——私钥解密");
String str = "365";
// System.out.println("\r明文:\r\n" + str);
// System.out.println("\r明文大小:\r\n" + str.getBytes().length);
String encodedData = RSAUtils.publicEncrypt(str, RSAUtils.getPublicKey(publicKey));
System.out.println("密文:\r\n" + encodedData);
String decodedData = RSAUtils.privateDecrypt(encodedData, RSAUtils.getPrivateKey(privateKey));
System.out.println("解密后文字: \r\n" + decodedData);
}
}
每次请求前都对需要token的请求进行拦截,验证token
TokenAspect:
package com.pinyu.miniprogram.global.aspect;
import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.pinyu.miniprogram.config.SpringBeanTool;
import com.pinyu.miniprogram.config.redis.RedisUtils;
import com.pinyu.miniprogram.global.exception.AuthTokenException;
import com.pinyu.miniprogram.mysql.entity.member.MemberEntity;
import com.pinyu.miniprogram.utils.RequestUtils;
import com.pinyu.miniprogram.utils.Token;
@Aspect
@Component("tokenAspect")
public class TokenAspect {
@Resource
private MappingJackson2HttpMessageConverter converter;
@Autowired
private SpringBeanTool springBeanTool;
@Autowired
private RedisUtils redisUtils;
@Autowired
private Token token;
private static final Logger log = LoggerFactory.getLogger(TokenAspect.class);
// 配置织入点
@Pointcut("@annotation(com.pinyu.miniprogram.global.ann.RequireToken)")
public void pointCut() {
}
/**
* 拦截token
*
* @param joinPoint
* @param e
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@Before(value = "pointCut()")
public void doBefore(JoinPoint joinPoint) throws Exception {
handleLog(joinPoint);
}
private void handleLog(JoinPoint joinPoint) throws Exception {
HttpServletResponse response = springBeanTool.getResponse();
HttpServletRequest request = springBeanTool.getRequest();
HttpOutputMessage outputMessage = new ServletServerHttpResponse(response);
String requestToken = RequestUtils.getHeader(request, "token");
if (StringUtils.isBlank(requestToken)) {
// 可以使用以下方式返回json数据
// converter.write(JsonMsg.Error(Code.TOKEN_VALIDA_NULL),MediaType.APPLICATION_JSON, outputMessage);
// shutdownResponse(response);
// 不要用,用了这个在controller@responseBody无效,输出流关闭了
throw new AuthTokenException(Token.TOKEN_VALIDA_NULL);
}
Double score = redisUtils.score(Token.MEMBER_TOKEN, requestToken);
if (score == null) {
// 终止继续往下面走,另外全局异常捕获AuthTokenException并给前端code码和提示
throw new AuthTokenException(Token.TOKEN_VALIDA_FAIL);
}
// 获取redis已有的member信息,不查数据库,重新生成token放入
MemberEntity member = (MemberEntity) redisUtils.get(requestToken);
// 移除之前的token(包含member信息、token排行信息)
token.delToken(member);
String saveToken = token.saveToken(member);
response.setHeader("Access-Control-Expose-Headers",
"Cache-Control,Content-Type,Expires,Pragma,Content-Language,Last-Modified,token");
response.setHeader("token", saveToken); // 设置响应头
}
private void shutdownResponse(HttpServletResponse response) throws IOException {
response.getOutputStream().close();
}
}
在token验证不通过,抛出异常中断请求进入controller方法并给予客户端提示:
AuthTokenException:
package com.pinyu.miniprogram.global.exception;
public class AuthTokenException extends Exception{
public AuthTokenException(String msgs){
super(msgs);
}
}
全局异常处理:GlobalControllerAdvice
package com.pinyu.miniprogram.global.exception;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.pinyu.miniprogram.utils.Token;
import com.pinyu.miniprogram.utils.result.JsonMsg;
/**
* @author ypp
* 创建时间:2018年10月15日 下午4:06:50
* @Description: TODO(全局异常处理)
*/
@ControllerAdvice
public class GlobalControllerAdvice implements ResponseBodyAdvice{
protected static Logger log=LogManager.getLogger(GlobalControllerAdvice.class);
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {}
/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
* @param model
*/
@ModelAttribute
public void addAttributes(Model model) {
// model.addAttribute("author", "Magical Sam");
}
/**
* 全局异常捕捉处理
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public String errorHandler(Exception ex) {
Throwable cause = ex.getCause();
if(cause instanceof AuthTokenException){
return Token.Error(cause.getMessage());
}
String sOut = ex.getClass().getName()+"\r\n";
StackTraceElement[] trace = ex.getStackTrace();
for (StackTraceElement s : trace) {
sOut += "\tat " + s + "\r\n";
}
log.error(sOut);
ex.printStackTrace();
return JsonMsg.Error("服务器异常,请联系管理员");
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter arg1, MediaType arg2,
Class extends HttpMessageConverter>> arg3, ServerHttpRequest arg4, ServerHttpResponse arg5) {
//可以在此处进行返回数据全局处理,body就是返回数据,还没有经过@ResponseBody处理
return body;
}
@Override
public boolean supports(MethodParameter arg0, Class extends HttpMessageConverter>> arg1) {
return true;// 只有返回true才会继续执行
}
}
SpringBeanTool:
package com.pinyu.miniprogram.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author ypp 创建时间:2018年10月17日 上午11:59:11
* @Description: TODO(用一句话描述该文件做什么)
*/
@Component
@WebListener
public class SpringBeanTool implements ApplicationContextAware, ServletContextListener {
/**
* 上下文对象实例
*/
private ApplicationContext applicationContext;
private ServletContext servletContext;
private static Environment env;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
env=applicationContext.getBean(Environment.class);
}
/**
* 获取applicationContext
*
* @return
*/
public ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 获取servletContext
*
* @return
*/
public ServletContext getServletContext() {
return servletContext;
}
/**
* 通过name获取 Bean.
*
* @param name
* @return
*/
public Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*
* @param clazz
* @param
* @return
*/
public T getBean(Class clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param
* @return
*/
public T getBean(String name, Class clazz) {
Assert.hasText(name, "name为空");
return getApplicationContext().getBean(name, clazz);
}
@Override
public void contextInitialized(ServletContextEvent sce) {
this.servletContext = sce.getServletContext();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
public HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
public HttpServletResponse getResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
public static String getValueApplicationPropertiesByKey(String key){
return env.getProperty(key);
}
}
允许跨域CorsConfig:
package com.pinyu.miniprogram.config;
import java.nio.charset.Charset;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOrigins("*")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*");
//跨域允许时间
// .maxAge(3600);
}
}
全局乱码处理WebAppConfigurer:
package com.pinyu.miniprogram.config;
import java.nio.charset.Charset;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.pinyu.miniprogram.global.interceptor.LoginInterceptor;
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
// @Bean
// public HttpMessageConverter responseBodyConverter(){
// //解决返回值中文乱码,除非返回值出现乱码情况,不然别设置,设置了以后@RequestBody绑定json参数可能会报错:Content type 'application/json;charset=UTF-8' not supported 需进一步处理
// StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
// return converter;
// }
//
// @Override
// public void configureMessageConverters(List> converters) {
// converters.add(responseBodyConverter());
// }
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 可添加多个,这里选择拦截所有请求地址
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
}
}
测试:
贴上注解@RequireToken表示需要token验证,AOP切面拦截贴了@RequireToken注解的请求
@RequestMapping("/findById-{id}")
@RequireToken
public String findById(@PathVariable("id") Long id) {
return JsonMsg.OK(service.findById(id));
}
测试:
上图中,我在header传入了token,这个方法是需要校验token的,但是提示token为空和token错误。然后进行登录,登录之后会返回token,使用该token进行查询findby-xx,是可以查询member信息的,再次使用token查询,第二次点击,验证错误(是因为这时的token已经不存在了,每请求一次token都是会变的)
再次测试:
还是登录之后生成token,登录之后token是需要返回到客户端保存的,下一次请求带token请求,请求之后返回新的token,一次类推,每次请求后的token不一样的。
以上示例根据自己的规则稍加改造一点是可以拿到实际应用中使用的!
这个项目示例包含了springboot redis集成、springboot整合elasticsearch集群、springboot整合activeMq和整合mybatis通用mapper以及token相关示例等
源码已上传github https://github.com/yfcgklypp/miniprogram.git