目录
一、案例前的准备
1、RSA公钥私钥生成
第一步:生成密钥
通过keyTool(java提供的证书管理工具)生成
只要安装了JDK,就有这个工具。进入jdk的安装目录,就能使用该工具:
输入以下命令生成私钥:
// 以下指令为一行,设置多行是方便阅读
keytool -genkeypair
-alias 密钥别名
-keyalg 使用的算法
-keypass 密钥的访问密码
-keystore 生成的密钥库文件名,扩展名是jks
-storepass 密钥库的访问密码,用来打开jks文件
此时私钥已生成:
第二步:生成公钥
对于微服务来说,私钥放在认证服务上,其他服务只需要存公钥即可。因为其他服务只做校验,不加密JWT。私钥加密,公钥解密。
生成公钥输入以下命令:
keytool -list -rfc --keystore jks文件(包含扩展名.jks) | openssl x509 -inform pem -pubkey
复制文本,重命名文件
公钥和私钥放到服务器上,私钥用于授权服务器授权token时加密,其他服务器仅需要通过公钥便可对加密内容进行解析了。
2、JWT通过RSA算法签发
这里需要添加依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
<version>1.0.11.RELEASE</version>
</dependency>
JWT工具类:
package com.dragonwu.utils;
import com.dragonwu.model.OnlineUser;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
/**
* 通过RSA非对称加密的JWT工具类
*
* @author DragonWu
* @date 2022-10-06 17:51
**/
public class JwtUtil {
//默认60分钟
public static final Long DEFAULT_EXPIRE_MINUTES = 60L;
//jwt的唯一标识,一般为username或userId
public static final String JWT_PRIMARY_KEY = "userId";
//私钥路径
public static final String PRIVATE_KEY_PATH = "/keys/dragonwu.jks";
//公钥路径
public static final String PUBLIC_KEY_PATH = "/keys/public.key";
//私钥
public static PrivateKey privateKey;
//公钥
public static PublicKey publicKey;
/**
* 传入的数据不得含敏感信息,否则可能被盗用,私钥加密token
*
* @param claims 数据体
* @param minutes 过期时间,分
* @return String JWT的token
*/
public static String createJwt(Map<String, Object> claims, long minutes) {
//获取私钥
if (Objects.isNull(privateKey)) {
privateKey = getPrivateKey();
}
//当前系统时间
long now = System.currentTimeMillis();
//过期时间
long exp = now + minutes * 60 * 1000;
JwtBuilder jwtBuilder = Jwts.builder()
//签发的算法
.signWith(SignatureAlgorithm.RS256, privateKey)
//设置令牌过期时间
.setExpiration(new Date(exp))
//设置签发时间
.setIssuedAt(new Date())
//设置body数据,自定义设置
.setClaims(claims);
return jwtBuilder.compact();
}
/**
* 公钥解析jwt
*
* @param jwt jwt字符串
* @return 返回null证明解析异常
*/
public static Map<String, Object> parseJwt(String jwt) {
if (Objects.isNull(publicKey)) {
publicKey = getPublicKey();
}
try {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(jwt).getBody();
} catch (Exception ignored) {
}
return null;
}
/**
* 获取私钥
*
* @return 当获取异常时返回null
*/
private static PrivateKey getPrivateKey() {
try {
ClassPathResource keyFileResource = new ClassPathResource(PRIVATE_KEY_PATH);
//创建秘钥工厂,参数为:秘钥文件、秘钥库密码
//import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
//pom文件:
/*
公钥私钥生成 https://blog.csdn.net/qq_37470815/article/details/123027798
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
<version>1.0.11.RELEASE</version>
</dependency>
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyFileResource, "密钥库密码password".toCharArray());
//获取秘钥,参数为:别名,秘钥密码
KeyPair keyContent = keyStoreKeyFactory.getKeyPair("alias别名", "秘钥的密码".toCharArray());
*/
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyFileResource, "dragonwu".toCharArray());
KeyPair keyContent = keyStoreKeyFactory.getKeyPair("dragonwu", "dragonwu".toCharArray());
return keyContent.getPrivate();
} catch (Exception ignored) {
}
return null;
}
/**
* 获取公钥
*
* @return 当获取异常时返回null
*/
private static PublicKey getPublicKey() {
try {
Resource publicKey = new ClassPathResource(PUBLIC_KEY_PATH);
InputStreamReader publicKeyIs = new InputStreamReader(publicKey.getInputStream());
BufferedReader publicKeyBr = new BufferedReader(publicKeyIs);
StringBuilder publicKeySb = new StringBuilder();
String line;
//将文件中的多行变为一行
while ((line = publicKeyBr.readLine()) != null) {
publicKeySb.append(line);
}
//将String转换成java的PublicKey对象
byte[] byteKey = Base64.getDecoder().decode(publicKeySb.toString());
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(byteKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(x509EncodedKeySpec);
} catch (Exception ignored) {
}
return null;
}
/*
SpringSecurity登录后获取用户信息
*/
public static OnlineUser getUser() {
return (OnlineUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
3、Redis的配置
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis配置类:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 设置 Key 的序列化 - String 序列化 RedisSerializer.string() => StringRedisSerializer.UTF_8
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置 Value 的序列化 - JSON 序列化 RedisSerializer.json() => GenericJackson2JsonRedisSerializer
redisTemplate.setValueSerializer(RedisSerializer.json());
redisTemplate.setHashValueSerializer(RedisSerializer.json());
// 返回
return redisTemplate;
}
}
redis工具类:
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/*
设置对象
*/
public void setObject(String key, Object object, long time, TimeUnit timeUnit) {
try {
redisTemplate.opsForValue().set(key, object, time, timeUnit);
} catch (Exception e) {
e.printStackTrace();
}
}
/*
获取对象
*/
public Object getObject(String key) {
try {
return redisTemplate.opsForValue().get(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/*
删除对象
*/
public void deleteObject(String key) {
try {
redisTemplate.delete(key);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、FastJson配置
引入依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.9</version>
</dependency>
fastJson配置类:
@Configuration
public class MyFastJsonConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
return new HttpMessageConverters((HttpMessageConverter<?>) fastConverter);
}
}
5、用户表与实体类
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(30) NOT NULL COMMENT '用户账号',
`password` varchar(100) DEFAULT '' COMMENT '密码'
PRIMARY KEY (`user_id`) USING BTREE
) ;
实体类:
@Data
@TableName("sys_user")
public class SysUser implements Serializable {
@TableId(value = "user_id")
private Long userId;
private String userName;
//对该字段不进行序列化
@JSONField(serialize = false)
private String password;
}
登录成功返回该前端的对象:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVo implements Serializable {
//用户对象
private SysUser sysUser;
//用户权限
private List<String> authorities;
//JWT token
private String token;
}
6、统一返回对象
package com.dragonwu.model;
import com.dragonwu.constant.Constants;
import java.io.Serializable;
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 成功
*/
public static final int SUCCESS = Constants.SUCCESS;
/**
* 失败
*/
public static final int FAIL = Constants.FAIL;
private int code;//状体码
private String msg;//信息
private T data;//数据
public static <T> R<T> ok() {
return restResult(null, SUCCESS, null);
}
public static <T> R<T> ok(T data) {
return restResult(data, SUCCESS, null);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, SUCCESS, msg);
}
public static <T> R<T> fail() {
return restResult(null, FAIL, null);
}
public static <T> R<T> fail(String msg) {
return restResult(null, FAIL, msg);
}
public static <T> R<T> fail(T data) {
return restResult(data, FAIL, null);
}
public static <T> R<T> fail(T data, String msg) {
return restResult(data, FAIL, msg);
}
public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
public static <T> R<T> restResult(T data, int code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
7、常量类
package com.dragonwu.constant;
/**
* @author DragonWu
* @since 2022-10-07 19:21
**/
public interface Constants {
/*
成功
*/
Integer SUCCESS = 200;
/*
失败
*/
Integer FAIL = 500;
/*
redis的统一前缀
*/
String REDIS_PRE = "DragonWu:";
/*
redis的登录用户前缀
*/
String ONLINE_PRE = REDIS_PRE + "online:";
/*
登录
*/
String LOGIN_OK = "登录成功";
String LOGIN_FAIL = "登录失败";
String LOGOUT_OK = "登出成功";
String LOGOUT_FAIL = "登出失败";
String LOGIN_ERROR = "用户名或密码错误";
/*
获取资源
*/
String GET_RESOURCE_OK = "获取成功";
String GET_RESOURCE_FAIL = "获取失败";
}
8、图片验证码的使用
依赖引入:
<!--验证码-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
服务层接口:
package com.dragonwu.service;
import com.dragonwu.constant.Constants;
import java.io.IOException;
import java.util.Map;
/**
* 验证码类
* https://github.com/whvcse/EasyCaptcha
*/
public interface EasyCaptchaService {
/*
验证码宽度
*/
Integer CAPTCHA_WIDTH = 130;
/*
验证码高度
*/
Integer CAPTCHA_HEIGHT = 48;
/*
验证码长度
*/
Integer CAPTCHA_LENGTH = 4;
/*
验证码类型: 默认图片
*/
int CAPTCHA_TYPE_DEFAULT_PNG = 10;
/*
验证码类型: 动态GIF
*/
final int CAPTCHA_TYPE_GIF = 20;
/*
验证码类型: 中文
*/
final int CAPTCHA_TYPE_CHINESE = 30;
/*
验证码类型: 动态中文
*/
final int CAPTCHA_TYPE_GIF_CHINESE = 40;
/*
验证码类型: 算术
*/
final int CAPTCHA_TYPE_ARITHMETIC = 50;
/*
验证码缓存key前缀
*/
String CAPTCHA_KEY_PRE = Constants.REDIS_PRE + "captcha:";
/*
验证码图片键名
*/
String IMAGE = "image";
/*
验证码UUID键名
*/
String UUID = "uuid";
/*
验证码缓存时间,分
*/
Integer CAPTCHA_CACHE_TIME = 3;
/**
* 生成普通图片验证码
*
* @param type 验证码类型
* @return Map 验证码值,和base64转换的图片
*/
Map<String, String> outputCaptchaImg(Integer type) throws IOException;
/**
* 校验图片验证码
*
* @param uuid 存入redis的key
* @param captchaCode 验证码
* @throws RuntimeException 验证失败抛出错误
*/
void verifyCaptchaCode(String uuid, String captchaCode) throws RuntimeException;
}
服务实现类:
package com.dragonwu.service.impl;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.dragonwu.service.EasyCaptchaService;
import com.dragonwu.utils.RedisUtil;
import com.dragonwu.utils.SnowflakeIdGen;
import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.ChineseCaptcha;
import com.wf.captcha.ChineseGifCaptcha;
import com.wf.captcha.GifCaptcha;
import com.wf.captcha.SpecCaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class EasyCaptchaServiceImpl implements EasyCaptchaService {
/*
redis工具
*/
@Autowired
private RedisUtil redisUtil;
/*
雪花算法生成唯一key
*/
@Autowired
private SnowflakeIdGen snowflakeIdGen;
@Override
public Map<String, String> outputCaptchaImg(Integer type) throws IOException {
String codeValue = "";
Map<String, String> map = new HashMap<>();
switch (type) {
case CAPTCHA_TYPE_DEFAULT_PNG:
SpecCaptcha specCaptcha = new SpecCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
codeValue = specCaptcha.text().toLowerCase();
map.put(EasyCaptchaService.IMAGE, specCaptcha.toBase64());
break;
case CAPTCHA_TYPE_GIF:
GifCaptcha gifCaptcha = new GifCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
codeValue = gifCaptcha.text().toLowerCase();
map.put(EasyCaptchaService.IMAGE, gifCaptcha.toBase64());
break;
case CAPTCHA_TYPE_CHINESE:
ChineseCaptcha chineseCaptcha = new ChineseCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
codeValue = chineseCaptcha.text();
map.put(EasyCaptchaService.IMAGE, chineseCaptcha.toBase64());
break;
case CAPTCHA_TYPE_GIF_CHINESE:
ChineseGifCaptcha chineseGifCaptcha = new ChineseGifCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LENGTH);
codeValue = chineseGifCaptcha.text();
map.put(EasyCaptchaService.IMAGE, chineseGifCaptcha.toBase64());
break;
case CAPTCHA_TYPE_ARITHMETIC:
ArithmeticCaptcha arithmeticCaptcha = new ArithmeticCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT);
codeValue = arithmeticCaptcha.text();
map.put(EasyCaptchaService.IMAGE, arithmeticCaptcha.toBase64());
break;
default:
throw new IOException("类型不存在");
}
//生成redis键名
String uuid = String.valueOf(snowflakeIdGen.nextId());
//存入redis
redisUtil.setObject(EasyCaptchaService.CAPTCHA_KEY_PRE + uuid, codeValue, EasyCaptchaService.CAPTCHA_CACHE_TIME, TimeUnit.MINUTES);
map.put(EasyCaptchaService.UUID, uuid);
return map;
}
@Override
public void verifyCaptchaCode(String uuid, String captchaCode) throws RuntimeException {
String cacheCaptchaCode = (String) redisUtil.getObject(EasyCaptchaService.CAPTCHA_KEY_PRE + uuid);
if (StringUtils.isBlank(cacheCaptchaCode)) {
throw new RuntimeException("验证码已过期,请重新获取");
}
if (!cacheCaptchaCode.equals(captchaCode.toLowerCase())) {
throw new RuntimeException("验证码错误");
}
}
}
9、雪花算法生成唯一id
package com.dragonwu.utils;
import org.springframework.stereotype.Component;
/**
* 采用twitter的雪花算法,生成有一定顺序且不重复的id,结果类型为64位的long型
*/
@Component
public class SnowflakeIdGen {
//集群id
private final long dataCenterId;
//机器id
private final long workerId;
//集群id的bit位数
private final long dataCenterIdBits = 5L;
//机器id的bit位数
private final long workerIdBits = 5L;
//序列号
private long sequenceId;
//上一次生成id使用的timestamp ,以毫秒为单位
private long lastTimestamp = 1L;
/**
* 若没有指定集群id和机器id,则默认均为0
*/
public SnowflakeIdGen() {
this(0, 0);
}
/**
* 指定集群id和机器id
*/
public SnowflakeIdGen(long dataCenterId, long workerId) {
//集群id的最大编号
long maxdataCenterId = ~(-1L << dataCenterIdBits);
if (dataCenterId < 0 || dataCenterId > maxdataCenterId) {
throw new RuntimeException(String.format("dataCenterId greater than %d or less than 0", maxdataCenterId));
}
//机器id的最大编号
long maxWorkerId = ~(-1L << workerIdBits);
if (workerId < 0 || workerId > maxWorkerId) {
throw new RuntimeException(String.format("workerId greater than %d or less than 0", maxWorkerId));
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
/**
* 生成全局唯一的id
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) { //出现这种情况,通常是由于机器时间出问题了
throw new RuntimeException("machine time error");
}
//同一时刻生成的id号
//序列号的bit位数
long sequenceIdBits = 12L;
if (timestamp == lastTimestamp) {
//序列号的掩码
long sequenceIdMask = ~(-1L << sequenceIdBits);
sequenceId = (sequenceId + 1) & sequenceIdMask;
if (sequenceId == 0) { //说明当前毫秒的序列号用完了,需从下个毫秒数开始重新计数
timestamp = nextTimestamp(lastTimestamp);
}
} else {
//否则序列号从0开始
sequenceId = 0L;
}
lastTimestamp = timestamp;
//生成最终结果时,集群id需移动的bit位数
long timestampShiftBits = sequenceIdBits + workerIdBits + dataCenterIdBits;
//生成最终结果时,集群id需移动的bit位数
long dataCenterIdShiftBits = sequenceIdBits + workerIdBits;
//生成最终结果时,机器id需移动的bit位数
//去掉过去的时间,即从指定时间(本例以2017-10-12 00:00:00)开始算,
// 大约可用69.5年(41位的时间位,最大值换成毫秒,再换算成年,大约69.5年)
//1507737600000为从1970-01-01 00:00:00到2017-10-12 00:00:00经过的毫秒数
long pastMills = 1507737600000L;
return ((timestamp - pastMills) << timestampShiftBits)
| (dataCenterId << dataCenterIdShiftBits)
| (workerId << sequenceIdBits)
| sequenceId;
}
/**
* 获取上次取数毫秒的下一时刻
*/
long nextTimestamp(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
10、web渲染工具类
package com.dragonwu.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* web渲染工具类
*
* @author DragonWu
* @since 2022-10-07 15:36
**/
public class WebUtil {
/**
* @param response 渲染对象
* @param jsonString 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String jsonString) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(jsonString);
} catch (IOException e) {
e.printStackTrace();
}
}
}
二、SpringSecurity的配置
依赖引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1、SpringSecurity配置类
package com.dragonwu.config;
import com.dragonwu.filter.TokenAuthenticationFilter;
import com.dragonwu.handler.AccessDeniedHandlerImpl;
import com.dragonwu.handler.AuthenticationEntryPointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.Collections;
/**
* @author DragonWu
* @since 2022-10-06 21:14
**/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限访问注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//允许匿名访问的
private static final String[] ALLOW_ASK = {
"/login",
"/captcha"
};
//总数允许访问的
private static final String[] ALWAYS_ALLOW_ASK = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",//用来获取支持的动作
"/swagger-resources",//用来获取api-docs的URI
"/swagger-resources/configuration/security",//安全选项
"/webjars/**",
"/swagger-ui.html",//以上为api文档接口访问路径
"/static/**",//放行静态资源
"**/open" //开放访问的资源
};
//自定义登录
@Autowired
private UserDetailsService userDetailsService;
//token过滤器
@Autowired
private TokenAuthenticationFilter tokenAuthenticationFilter;
//禁止访问异常处理器
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
//认证异常处理器
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
/*
密码加密器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/*
设置自定义登录逻辑
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/*
自定义登录,用户认证
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/*
页面资源授权
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对应登录接口允许匿名访问
.antMatchers(ALWAYS_ALLOW_ASK).permitAll()
.antMatchers(ALLOW_ASK).anonymous()
//除上面接口全都需要鉴权访问
.anyRequest().authenticated();
//添加过滤器
http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
//添加异常处理器
http.exceptionHandling()
//认证失败的处理器
.authenticationEntryPoint(authenticationEntryPoint)
//授权失败处理器
.accessDeniedHandler(accessDeniedHandler);
//跨域配置
http.cors()
.configurationSource(corsConfigurationSource());
}
/*
跨域配置对象
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
//配置允许访问的服务器域名
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
2、SpringSecurity异常处理器
认证异常处理器:
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
R<String> result = R.restResult(null, HttpStatus.UNAUTHORIZED.value(), "认证失败");
String json = JSON.toJSONString(result);
//处理异常,返回结果
WebUtil.renderString(httpServletResponse, json);
}
}
禁止访问异常处理器:
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
R<String> result = R.restResult(null, HttpStatus.FORBIDDEN.value(), "禁止访问");
String json = JSON.toJSONString(result);
//处理异常,返回结果
WebUtil.renderString(httpServletResponse, json);
}
}
异常处理器可以将异常自定义处理。
三、JWT+SpringSecurity实现登录的主要步骤
1、登录图解
2、登录开始前首先创建UserDetails实体类的子类
package com.dragonwu.model;
import com.alibaba.fastjson.annotation.JSONField;
import com.dragonwu.model.entity.SysUser;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.tomcat.util.buf.StringUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author DragonWu
* @since 2022-10-06 20:29
**/
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
//用户角色权限
private List<String> jurisdiction;
//将不会被序列化到redis里
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginUser(SysUser sysUser, List<String> jurisdiction) {
this.sysUser = sysUser;
this.jurisdiction = jurisdiction;
}
public String getJurisdictionToString() {
return StringUtils.join(jurisdiction, ',');
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//把jurisdiction里的权限信息字符串封装成SimpleGrantedAuthority对象
if (!Objects.isNull(authorities)) {
return authorities;
}
authorities = jurisdiction.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3、创建UserDetailsService的实现类
通过UserDetailsService的实现可对用户对象进行自定义返回
package com.dragonwu.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dragonwu.model.entity.SysUser;
import com.dragonwu.mapper.SysUserMapper;
import com.dragonwu.model.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* @author DragonWu
* @since 2022-10-06 20:47
**/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName,username));
//如果没有查询到用户就抛出异常
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户名不存在!");
}
//查询对应的权限信息和角色信息
// 这一般需要查询role表和jurisdiction表的权限,这里略过
List<String> jurisdiction=new ArrayList<>(Arrays.asList("admin","normal","ROLE_manager"));
//将数据封装到UserDetail返回
return new LoginUser(user, jurisdiction);
}
}
4、登录的实现类与接口
接口:
package com.dragonwu.controller;
import com.alibaba.fastjson.JSONObject;
import com.dragonwu.constant.Constants;
import com.dragonwu.model.R;
import com.dragonwu.model.param.LoginParam;
import com.dragonwu.model.vo.LoginVo;
import com.dragonwu.service.LoginService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@Slf4j
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public R<LoginVo> login(@RequestBody LoginParam loginParam) {
try {
return R.ok(loginService.login(loginParam), Constants.LOGIN_OK);
} catch (RuntimeException msg) {
//返回失败结果
return R.fail(null, msg.getMessage());
} catch (Exception e) {
log.error("后台登录出现异常:" + JSONObject.toJSONString(loginParam), e);
return R.fail(null, Constants.LOGIN_FAIL);
}
}
@GetMapping("/logout.do")
public R<String> logout() {
try {
loginService.logout();
return R.ok(null, Constants.LOGOUT_OK);
} catch (RuntimeException msg) {
return R.fail(null, msg.getMessage());
} catch (Exception e) {
log.error("退出登录出现异常:" + e);
return R.fail(null, Constants.LOGOUT_FAIL);
}
}
@GetMapping("/captcha")
public R<Map<String, String>> captcha() {
try {
return R.ok(loginService.getCaptcha(), Constants.GET_RESOURCE_OK);
} catch (RuntimeException msg) {
return R.fail(null, msg.getMessage());
} catch (Exception e) {
log.error("获取验证码出现异常:", e);
return R.fail(null, Constants.GET_RESOURCE_FAIL);
}
}
}
服务层接口:
public interface LoginService {
/**
* 用户登录
* @param loginParam 登录参数
* @return 登录返回对象
*/
LoginVo login(LoginParam loginParam);
/**
* 退出登录
*/
void logout() throws RuntimeException;
/**
* 获取验证码
* @return Map UUID验证码对应redis里的key,image的base64编码
*/
Map<String,String> getCaptcha() throws IOException;
}
服务层实现类:
package com.dragonwu.service.impl;
import com.dragonwu.constant.Constants;
import com.dragonwu.model.LoginUser;
import com.dragonwu.model.OnlineUser;
import com.dragonwu.model.param.LoginParam;
import com.dragonwu.model.vo.LoginVo;
import com.dragonwu.service.EasyCaptchaService;
import com.dragonwu.service.LoginService;
import com.dragonwu.utils.JwtUtil;
import com.dragonwu.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author DragonWu
* @since 2022-10-07 19:02
**/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisUtil redisUtil;
@Autowired
private EasyCaptchaService easyCaptchaService;
@Override
public LoginVo login(LoginParam loginParam) throws RuntimeException {
//判断验证码是否正确
easyCaptchaService.verifyCaptchaCode(loginParam.getUuid(), loginParam.getCaptcha());
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginParam.getUsername(), loginParam.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过给出相应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException(Constants.LOGIN_ERROR);
}
//如果认证通过了,获取UserDetailService返回过期的UserDetail对象
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//使用primaryKey生成JWT
String primaryKey = loginUser.getSysUser().getUserId().toString();
Map<String, Object> claims = new HashMap<>();
claims.put(JwtUtil.JWT_PRIMARY_KEY, primaryKey);
String token = JwtUtil.createJwt(claims, JwtUtil.DEFAULT_EXPIRE_MINUTES);
//把用户信息存入redis
OnlineUser onlineUser = new OnlineUser(loginUser.getSysUser(), loginUser.getJurisdictionToString());
redisUtil.setObject(Constants.ONLINE_PRE + primaryKey, onlineUser, JwtUtil.DEFAULT_EXPIRE_MINUTES, TimeUnit.MINUTES);
//将返回结果封装达到返回对象
return new LoginVo(loginUser.getSysUser(), loginUser.getJurisdiction(), token);
}
@Override
public void logout() throws RuntimeException {
try {
//获取SecurityContextHolder里的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
OnlineUser onlineUser = (OnlineUser) authentication.getPrincipal();
String primaryKey = String.valueOf(onlineUser.getSysUser().getUserId());
//删除redis中的值
redisUtil.deleteObject(Constants.ONLINE_PRE + primaryKey);
} catch (Exception e) {
throw new RuntimeException(Constants.LOGOUT_FAIL);
}
}
@Override
public Map<String, String> getCaptcha() throws IOException {
return easyCaptchaService.outputCaptchaImg(EasyCaptchaService.CAPTCHA_TYPE_GIF);
}
}
这里我们便实现了登录逻辑。
5、认证过滤器
判断用户是否已登录
package com.dragonwu.filter;
import com.alibaba.fastjson.JSON;
import com.dragonwu.constant.Constants;
import com.dragonwu.model.OnlineUser;
import com.dragonwu.model.R;
import com.dragonwu.utils.JwtUtil;
import com.dragonwu.utils.RedisUtil;
import com.dragonwu.utils.WebUtil;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
/**
* token验证过滤器
*
* @author DragonWu
* @since 2022-10-07 19:02
**/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
public static final String HEADER = "Authorization";
public static final String PARAM = "token";
@Autowired
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获得前端请求中的token
String token = request.getHeader(HEADER);
if (StringUtils.isBlank(token)) {
token = request.getParameter(PARAM);
}
//如果token为空,放行,验证失败
if (StringUtils.isBlank(token)) {
chain.doFilter(request, response);
return;
}
//解析token
Map<String, Object> claims = JwtUtil.parseJwt(token);
if (Objects.isNull(claims)) {//解析异常
R<String> result = R.restResult(null, HttpStatus.FORBIDDEN.value(), "token非法");
String json = JSON.toJSONString(result);
//返回解析错误的json数据
WebUtil.renderString(response, json);
return;
}
//获取用户主键,以便到redis中查询
String primaryKey = (String) claims.get(JwtUtil.JWT_PRIMARY_KEY);
//从redis中获取token对应的数据
String redisKey = Constants.ONLINE_PRE + primaryKey;
//获取用户信息
OnlineUser onlineUser = (OnlineUser) redisUtil.getObject(redisKey);
if (Objects.isNull(onlineUser)) {
R<String> result = R.restResult(null, HttpStatus.UNAUTHORIZED.value(), "认证失败");
String json = JSON.toJSONString(result);
//返回请求失败的json数据
WebUtil.renderString(response, json);
return;
}
//存入SecurityContextHolder中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(onlineUser, null,
AuthorityUtils.commaSeparatedStringToAuthorityList(onlineUser.getAuthorities()));
//获取权限信息封装到authenticationToken里
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
chain.doFilter(request, response);
}
}
四、测试
1、登录测试
首先访问验证码接口获取验证码的图片和uuid:
服务器通过用户输入的验证码结果和redis中的缓存进行比较得出是否验证码正确。
访问登录接口:
登录成功:
2、资源访问测试
未携带JWT:
携带token就是登录的状态:
携带错误的token测试:可以看到错误的token会被服务器警告。
3、退出登录
退出登录后,redis中缓存的用户信息将被删除。
五、案例源码链接
SpringCloudAlibaba各技术栈实现的案例代码集 我的笔记: SpringCloudAlibaba各技术栈实现的案例代码集 我的笔记 - Gitee.com