继上篇
对于安全系数要求高的登录系统
通常会采用以下措施:
注意:jwt值可以看作一个token,下面会有两对公钥私钥,分别为public_key,private_key,public_sign_key,private_sign_key
- 前端的账号密码验证码传输时,使用后端提供的公钥加密(public_key)也可以使用对称加密,即前后台使用一个密钥),并生成一个uuid(uuid需存在前端缓存
sessionStorage里)方便后续使用 - 后端接收uuid后,将
uuid作为key的一部分,jwt的值私钥签名(private_sign_key)之后作为value值,后续后端存入redis中。 - 先进行私钥解密(private_key)就能获取前端的账号密码验证码信息,后端做登录密码验证时,需要将私钥解密的数据(即前端传过来的密码)再次
加密(密码加密,一般可自行MD5+盐加密或者使用PasswordEncoder内置的实现方法),再和数据库里的密码(存的加密后的数据)比对,一致则登录成功,否则登录失败。 - 前端调用后台接口前都需要验签(白名单接口除外),此时需要将sessionStorage中的uuid和参数的其他的参数一并传到后台接口,后台接口获取请求参数中的uuid,并去查找存在redis中的jwt值签名值,并用公钥解签(public_sign_key)这个值(验证jwt是否是系统颁发的)
- 验签通过之后即可调用接口的信息
代码实现
前端代码(vue)
jsencrypt.js 用户密码验证码等信息加密
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = '你的数据传输公钥'
// 账号密码传输加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
login.vue 登录页面
<template>
<div class="center">
<el-form :rules="rules" :model="state.form" label-width="100px" style="max-width: 360px">
<el-form-item prop="username" label="账号">
<el-input type="text" id="username" v-model="state.form.username" required/>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input type="password" id="password" v-model="state.form.password" required/>
</el-form-item>
<el-form-item prop="captcha" label="验证码">
<el-row justify="space-between">
<el-col :span="12">
<el-input v-model="state.form.captcha" class="custom-input-login" placeholder="请输入验证码"></el-input>
</el-col>
<el-col :span="11">
<Captcha ref="captchaRef" />
</el-col>
</el-row>
</el-form-item>
<el-button type="primary" plain @click="login">登录</el-button>
<p v-if="state.error != null" class="error">{{ state.error }}</p>
</el-form>
</div>
</template>
<script>
import request from "@/util/request";
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { encrypt } from "@/util/jsencrypt";
import Captcha from "@/components/Captcha.vue";
export default {
// 引入组件
components: {
Captcha,
},
// 引入路由
setup() {
const userRouter = useRouter();
// 引入loading
const state = reactive({
form: {
username: "",
password: "",
captcha: "", // 验证码
uuid: "", // uuid
},
error: "",
});
// 表单验证
const rules = {
username: [
{ required: true, message: "请输入账号", trigger: "blur" },
{
min: 5,
max: 15,
message: "账号长度为 5 到 15 个字符",
trigger: "blur",
},
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{
min: 6,
max: 20,
message: "密码长度为 6 到 20 个字符",
trigger: "blur",
},
],
captcha: [{ required: true, message: "请输入验证码", trigger: "blur" }],
};
// 登录
const login = () => {
try {
let uuid = generateUUID();
// 将账号密码单独加密,也可以一起加密,看个人选择
const formData = {
username: encrypt(state.form.username),
password: encrypt(state.form.password),
captcha: state.form.captcha,
uuid: uuid,
};
request.post("/login", formData).then((res) => {
if (res.data != null) {
sessionStorage.setItem("uuid", uuid);//缓存uuid信息后续需要uuid+用户去拿token信息
userRouter.push("/index"); //登录成功之后进行页面的跳转,跳转到主页
} else {
state.error = "登录失败";
captchaRef.value.refreshCaptcha(); // 刷新验证码
}
});
} catch (error) {
state.error = "登录失败";
captchaRef.value.refreshCaptcha(); // 刷新验证码
}
};
// 生成uuid
const generateUUID = () => {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0,
v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
}
);
};
// 引入验证码
const captchaRef = ref(null);
return {
state,
login,
loginWithWechat,
captchaRef,
rules,
generateUUID,
};
},
};
</script>
<style>
.center {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
width: 100%;
height: 100%;
}
.error {
color: rgb(201, 43, 43);
font-size: 30px;
}
</style>
Captcha.vue 验证码刷新组件
<template>
<div class="captcha">
<img :src="captchaSrc" @click="refreshCaptcha" alt="验证码" />
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import request from "@/util/request";
// 定义一个变量来存储验证码的 base64 字符串
const captchaSrc = ref("");
// 接收父组件传递的 uuid
const emit = defineEmits(["uuidReceived"]);
// 刷新验证码
const refreshCaptcha = async () => {
request.post("/login/getCaptcha").then((res) => {
if (res && res.data && res.data.captcha) {
captchaSrc.value = `data:image/png;base64,${res.data.captcha}`;
if (res.data.uuid) {
// 向父组件传递 uuid
emit("uuidReceived", res.data.uuid);
}
}
});
};
// 组件挂载时刷新验证码
onMounted(() => {
refreshCaptcha();
});
// 向外暴露刷新验证码的方法
defineExpose({
refreshCaptcha,
});
</script>
<style scoped>
.captcha img {
cursor: pointer;
border: 1px solid #dcdcdc;
border-radius: 4px;
}
</style>
request.js
import axios from 'axios'
const request = axios.create({
baseURL: '/knowledge', // 注意!! 这里是全局统一加上了 '/knowledge' 前缀,也就是说所有接口都会加上'/knowledge'前缀在,页面里面写接口的时候就不要加 '/knowledge'了,否则会出现两个'/knowledge',类似 '/knowledge/knowledge/user'这样的报错,切记!!!
timeout: 5000 // 请求超时时间
})
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
// 将uuid传到后端,方便后续验签
config.headers['uuid'] = sessionStorage.getItem("uuid"); // 设置请求头
return config
}, error => {
return Promise.reject(error)
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
return res;
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default request
后端代码(java)
/**
* SpringSecurity身份认证提供者(这是自定义的,方便加自定义的校验等)
*/
@Component
public class SpringSecurityAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* 用户身份认证服务接口
*/
@Autowired
private IUserAuthenticationService userAuthenticationService;
/**
* 密码加密器
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 密码加密 使用内置的BCryptPasswordEncoder加密,也可自定义实现加密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 根据用户名或其他凭证来查找用户,并返回一个包含用户详细信息的 UserDetails 对象
*
* @param username 用户名
* @param usernamePasswordAuthenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
try {
// 解密账号
username = RSAUtils.privateDecrypt(username, RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
UserDetails loadedUser = this.userAuthenticationService.loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (UsernameNotFoundException | InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
/**
* 用于在用户密码验证之后执行额外的身份验证检查。
* 一旦用户的密码验证成功,Spring Security 将调用这个方法,以便执行自定义的额外检查。
* 这些额外的检查可以包括检查用户是否被锁定、密码是否已过期、账户是否被禁用等。
* 这个方法允许你在完成基本的密码验证后,添加任何自定义的身份验证逻辑
*
* @param userDetails 后端查出来的用户对象(账号&&密码)
* @param userToken 前端传过来的用户对象(账号&&密码)
* @throws AuthenticationException
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken userToken) throws AuthenticationException {
// 账号
Object principal = userToken.getPrincipal();
// 密码
Object credentials = userToken.getCredentials();
if (principal == null || credentials == null) {
throw new BadCredentialsException("账号或密码不能为空!");
}
String username;
String password;
try {
// 解密账号
username = RSAUtils.privateDecrypt(String.valueOf(principal), RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
// 解密密码
password = RSAUtils.privateDecrypt(String.valueOf(credentials), RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
} catch (Exception e) {
throw new BadCredentialsException("账号或密码解密错误!");
}
// 拿到后端查询到的账号密码信息进行比较
if (!StringUtils.equals(username, userDetails.getUsername())) {
throw new BadCredentialsException("账号或密码错误!");
}
/**
* 比较用户输入的密码加密之后的值与数据库里存的是否一致
* 密码加密比对过程:
* 数据库密码解析获取其中的 随机盐 的值,然后将这个盐和用户输入的密码通过同样的加密方式加密
* 然后比对这个密码和数据库的加密值是否一致
*/
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("账号或密码错误!");
}
}
}
SpringSecurity核心类 SpringSecurityConfig
@Configuration
@EnableWebSecurity(debug = true)
@Slf4j
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* SpringSecurity身份认证提供者(自定义的身份认证类)
*/
@Resource
private SpringSecurityAuthenticationProvider securityAuthenticationProvider;
/**
* JWT配置属性(方便获取yml文件中的值,用来接收有关于jwt的属性)
*/
@Resource
private JwtProperties jwtProperties;
/**
* JWT的工具类(里面包含了签名和验签等)
*/
@Autowired
private JwtTokenUtils jwtTokenUtils;
/**
* redis操作组件
*/
@Resource
private RedisComponent redisComponent;
/**
* 登录时忽略URL,即以这个开头的不需要登录验证即可访问
*/
protected static final String[] PUBLC_URLS = {
"/login",
"/user/login",
"/login/getCaptcha",
"/register"
};
/**
* 用于配置认证管理器,包括用户认证、角色授权等。您可以在此方法中配置内存用户、数据库用户、LDAP 用户等
* 身份验证管理器生成器
* this.securityAuthenticationProvider就是自定义的那个类
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.securityAuthenticationProvider);
}
/**
* 用于配置 Web 安全设置,包括忽略某些 URL 的过滤器链、静态资源的安全配置等
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略对以 "/public/" 开头的静态资源的访问控制,这样用户可以在不需要进行身份验证的情况下访问这些静态资源。
web.ignoring().mvcMatchers("/public/**").requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
/**
* 定义登陆成功返回信息
*/
private class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
// 接收前端请求的请求头中的uuid用于后续操作
String uuid = request.getParameter("uuid");
// 组装JWT
SpringSecurityActiveUser securityActiveUser = (SpringSecurityActiveUser) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成访问令牌
String accessToken = jwtTokenUtils.createAccessToken(uuid, securityActiveUser.getUsername());
// 生成刷新令牌,如果accessToken令牌失效,则使用refreshToken重新获取令牌(refreshToken过期时间必须大于accessToken)
String refreshToken = jwtTokenUtils.creatRefreshToken(accessToken);
// 存入Redis
redisComponent.setObj(RedisConstants.ACCESS_TOKEN_KEY + "_" + uuid, accessToken, jwtProperties.getAccessTokenExpiration(), TimeUnit.SECONDS);
redisComponent.setObj(RedisConstants.REFRESH_TOKEN_KEY + "_" + uuid, refreshToken, jwtProperties.getRefreshTokenExpiration(), TimeUnit.SECONDS);
redisComponent.setObj(RedisConstants.ACTIVE_USER_KEY + "_" + uuid, JSON.toJSONString(securityActiveUser, SerializerFeature.WriteMapNullValue), jwtProperties.getRefreshTokenExpiration(), TimeUnit.SECONDS);
// 设置返回值
LoginTokenResponse result = new LoginTokenResponse();
Result<?> success = Result.success(result);
Result.responseJson(success, response);
}
}
/**
* 用于配置 HTTP 安全设置,包括 URL 访问权限、表单登录、注销、CSRF 保护等
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 配置授权规则
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers(PUBLC_URLS).permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
)
// 配置表单登录
.formLogin(formLogin ->
formLogin.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.successForwardUrl("/index")
.successHandler(new AjaxAuthSuccessHandler())
.failureHandler(new AjaxAuthFailHandler()).permitAll()
)
// 配置登出
.logout(logout ->
logout.logoutUrl("/logout").logoutSuccessHandler(new AjaxLogoutSuccessHandler())
)
// CSRF禁用,因为不使用session
.csrf(csrf ->
csrf.disable()
)
//禁用session,JWT校验不需要session,需要禁用它
.sessionManagement(sessionManage ->
sessionManage.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置异常处理
.exceptionHandling(exceptionHandling ->
exceptionHandling.accessDeniedPage("/error/403")
)
// 配置HTTP标头安全性
.headers(headers ->
headers.frameOptions().sameOrigin().contentSecurityPolicy("frame-ancestors 'self'")
)
// 配置会话管理
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)
// 配置请求缓存
.requestCache(requestCache ->
requestCache.requestCache(new HttpSessionRequestCache())
)
// 配置匿名用户的安全性
.anonymous(anonymous ->
anonymous.authorities("ROLE_ANONYMOUS")
)
// 配置HTTP Basic认证(支持基本认证,因为 OAuth2 认证往往用于不同种类客户端,所以基本认证支持是必要的)
.httpBasic(httpBasic -> {
httpBasic.realmName("My Realm");
})
// 配置Remember-Me功能
.rememberMe(rememberMe -> {
rememberMe.key("uniqueAndSecret");
})
// 配置跨域资源共享(CORS)
.cors(cors -> {
cors.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
})
// 添加自定义过滤器,在登录验证之前执行
.addFilterBefore(new TokenAuthenticationFilter(this.jwtProperties, this.jwtTokenUtils, this.redisComponent), UsernamePasswordAuthenticationFilter.class);
}
}
/**
* 校验token的过滤器,直接获取header中的token进行校验
*
* @author mjr
* @date 2024/12/11 17:22
**/
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* JWT配置属性
*/
private JwtProperties jwtProperties;
/**
* JWT的工具类
*/
private JwtTokenUtils jwtTokenUtils;
/**
* redis操作组件
*/
private RedisComponent redisComponent;
public TokenAuthenticationFilter(JwtProperties jwtProperties, JwtTokenUtils jwtTokenUtils, RedisComponent redisComponent) {
this.jwtProperties = jwtProperties;
this.jwtTokenUtils = jwtTokenUtils;
this.redisComponent = redisComponent;
}
/**
* token存在则校验token
* 1. token是否存在
* 2. token存在:
* 2.1 校验token中的用户名是否失效
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 不需要认证的接口,直接放行
String uri = StringUtils.removeStart(request.getRequestURI(), "/knowledge-base");
// 检查请求的URI是否匹配任何一个模式
for (String pattern : this.jwtProperties.getAntMatchers()) {
if (this.pathMatcher.match(pattern, uri)) {
// 继续执行下一个过滤器
filterChain.doFilter(request, response);
return;
}
}
String uuid = request.getHeader("uuid");
if (!StringUtils.isEmpty(uuid)) {
// SecurityContextHolder.getContext().getAuthentication()==null 未认证则为true
if (!StringUtils.isEmpty(uuid) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 在线用户
Object securityActiveUserObj = this.redisComponent.getObj(RedisConstants.ACTIVE_USER_KEY + "_" + uuid);
String securityActiveUserJsonStr = securityActiveUserObj != null ? String.valueOf(securityActiveUserObj) : null;
SpringSecurityActiveUser securityActiveUser = securityActiveUserJsonStr != null ? JSON.parseObject(securityActiveUserJsonStr, SpringSecurityActiveUser.class) : null;
// Redis中的accessToken
Object accessTokenObj = this.redisComponent.getObj(RedisConstants.ACCESS_TOKEN_KEY + "_" + uuid);
String accessToken = accessTokenObj != null ? String.valueOf(accessTokenObj) : null;
if (accessToken != null && !"".equals(accessToken)) {
// 将用户信息存入 authentication,方便后续校验
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(securityActiveUser,
securityActiveUser.getPassword(),
securityActiveUser.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将 authentication 存入 ThreadLocal,方便后续获取用户信息
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}
// 继续执行下一个过滤器
filterChain.doFilter(request, response);
}
}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
……其他的导包自行导入
/**
* 自定义用户信息,可以根据需要把用户数据权限,访问权限等一同封装进去
*
* @author mjr
* @date 2024/12/13 16:49
**/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SpringSecurityActiveUser extends User implements Serializable {
private static final long serialVersionUID = 1L;
private Integer userId;
private String username;
private String password;
private String nickName;
private Integer age;
private String sex;
private String address;
private String avatar;
private BigDecimal account;
public SpringSecurityActiveUser(Integer userId, String username, String password, String nickName,
Integer age, String sex, String address, String avatar, BigDecimal account,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.userId = userId;
this.username = username;
this.password = password;
this.nickName = nickName;
this.age = age;
this.sex = sex;
this.avatar = avatar;
this.address = address;
this.account = account;
}
}
/**
* redis服务组件
*
* @author: mjr
* @date: 2024/12/10 15:10
*/
@Component
public class RedisComponent {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 把数据存入redis
*
* @param key
* @param obj
* @param timeout 超时时间 单位:分钟
*/
public void setObj(final String key, Object obj, long timeout) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, obj, timeout, TimeUnit.SECONDS);
}
/**
* 把数据存入redis
*
* @param key
* @param obj
* @param timeout 超时时间
* @param timeUnit 单位
*/
public void setObj(final String key, Object obj, long timeout, TimeUnit timeUnit) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, obj, timeout, timeUnit);
}
/**
* 把数据存入redis
*
* @param key
* @param obj
*/
public void setObj(final String key, Object obj) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, obj);
}
/**
* 从redis拿数据
*
* @param key
* @return
*/
public Object getObj(final String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 从redis中删除数据
*
* @param key 可以传一个值或多个值
* @return
*/
@SuppressWarnings("unchecked")
public void removeObj(final String... key) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
/**
* Redis数据库相关常量
*
* @author: mjr
* @date: 2024/12/10 15:15
*/
public class RedisConstants {
public static final String KNOWLEDGR_BASE_SECURITY = "KNOWLEDGR_BASE_SECURITY:SECURITY:";
public static final String ACCESS_TOKEN_KEY = KNOWLEDGR_BASE_SECURITY + "access_token_key_";
public static final String REFRESH_TOKEN_KEY = KNOWLEDGR_BASE_SECURITY + "refresh_token_key_";
public static final String ACTIVE_USER_KEY = KNOWLEDGR_BASE_SECURITY + "active_user_key_";
public static final String CAPTCHA_KEY = KNOWLEDGR_BASE_SECURITY + "captcha_key_";
}
/**
* 加解密相关常量
*/
public final class SecretConstants {
/**
* 数据加密RSA公钥
*/
public static final String DATA_RSA_PUBLIC_KEY = PropertiesUtils.readProperty("secret.properties",
"public_rsa_key");
/**
* 数据解密RSA私钥
*/
public static final String DATA_RSA_PRIVATE_KEY = PropertiesUtils.readProperty("secret.properties",
"private_rsa_key");
/**
* 数据验签公钥
*/
public static final String SIGN_RSA_PUBLIC_KEY = PropertiesUtils.readProperty("secret.properties",
"public_rsa_sign_key");
/**
* 数据签名私钥
*/
public static final String SIGN_RSA_PRIVATE_KEY = PropertiesUtils.readProperty("secret.properties",
"private_rsa_sign_key");
}
/**
* java读取.properties文件
*/
public class PropertiesUtils {
private final static Logger LOGGER = LoggerFactory.getLogger(PropertiesUtils.class);
/**
* 读取资源属性文件(properties),无缓存方式
*
* @param filePath
* @param param
* @return
*/
public static String readPropertyNoCache(String filePath, String param) {
try {
String url = PropertiesUtils.class.getResource("/").getPath() + filePath;
Properties prop = new Properties();
InputStream in = new BufferedInputStream(new FileInputStream(url));
prop.load(new InputStreamReader(in, "utf-8"));
return prop.getProperty(param);
} catch (IOException e) {
LOGGER.error("读properties属性文件异常!", e);
}
return null;
}
/**
* 读取资源属性文件(properties),用IO流的方式
*
* @param filePath
* @param param
* @return
*/
public static String readProperty(String filePath, String param) {
// 属性集合对象
Properties properties = new Properties();
// 获取路径并转换成流
InputStream path = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
try {
// 将属性文件流装载到Properties对象中
properties.load(path);
return properties.getProperty(param);
} catch (IOException e) {
LOGGER.error("读properties属性文件异常!", e);
}
return null;
}
/**
* 读取资源属性文件(properties),然后根据.properties文件的名称信息(本地化信息)
*
* @param filePath
* @param param
* @return
*/
public static String getProperty(String filePath, String param) {
ResourceBundle resourceBundle = ResourceBundle.getBundle(filePath);
return resourceBundle.getString(param);
}
/**
* 读取.properties配置文件的内容至Map中
*
* @param propertiesFile
* @param param
* @return
*/
public static Map<String, String> read2Map(String propertiesFile, String param) {
ResourceBundle rb = ResourceBundle.getBundle(propertiesFile);
Map<String, String> map = new HashMap<String, String>(16);
Enumeration<String> enu = rb.getKeys();
while (enu.hasMoreElements()) {
String obj = enu.nextElement();
// 传了参数
if (StringUtils.isNotEmpty(param)) {
if (obj.indexOf(param) != -1) {
String objv = rb.getString(obj);
map.put(obj, objv);
}
}
// 没传参数
else {
String objv = rb.getString(obj);
map.put(obj, objv);
}
}
return map;
}
/**
* 写properties文件
*
* @param filePath
* @param pKey
* @param pValue
* @return
*/
public static boolean writeProperties(String filePath, String pKey, String pValue) {
try {
String url = PropertiesUtils.class.getResource("/").getPath() + filePath;
Properties prop = new Properties();
InputStream in = new BufferedInputStream(new FileInputStream(url));
// 将属性文件流装载到Properties对象中
prop.load(in);
// 调用 Hashtable 的方法 put。使用 getProperty 方法提供并行性。
// 强制要求为属性的键和值使用字符串。返回值是 Hashtable 调用 put 的结果。
OutputStream out = new FileOutputStream(url);
prop.setProperty(pKey, pValue);
// 以适合使用 load 方法加载到 Properties 表中的格式,
// 将此 Properties 表中的属性列表(键和元素对)写入输出流
prop.store(out, "Update " + pKey + " name");
return true;
} catch (Exception e) {
LOGGER.error("写properties属性文件异常!", e);
return false;
}
}
}
secret.properties文件内容
secret.properties文件里的值可以去在线网站生成密钥对 web.chacuo.net/netrsakeypa… 也可以代码生成
代码生成
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import org.apache.commons.codec.binary.Base64;
/**
* 公钥私钥生成程序
*
* @author: mjr
* @date: 2024/12/6 10:32
*/
public class RSAUtil {
private static final int KEY_SIZE = 1024;
public static void main(String[] args) throws Exception {
// 生成密钥对
KeyPair keyPair = generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
PrivateKey privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
System.out.println(publicKeyStr);
System.out.println("-------------------");
System.out.println(privateKeyStr);
System.out.println("-------------------");
// 原始数据
String originalData = "This is a secret message.";
byte[] data = originalData.getBytes();
// 加密
String encryptedData = encrypt(data, publicKey);
System.out.println("Encrypted Data: " + encryptedData);
// 解密
String decryptedData = decrypt(encryptedData, privateKey);
System.out.println("Decrypted Data: " + decryptedData);
}
/**
* 生成密钥对
*
* @return
* @throws Exception
*/
private static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(KEY_SIZE);
return keyPairGenerator.generateKeyPair();
}
/**
* 公钥加密数据
*
* @param data
* @param publicKey
* @return
* @throws Exception
*/
private static String encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data, KEY_SIZE));
}
/**
* 私钥解密数据
*
* @param encryptedData
* @param privateKey
* @return
* @throws Exception
*/
private static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(encryptedData), KEY_SIZE), "UTF-8");
}
/**
* 将数据分块
*
* @param cipher
* @param opmode
* @param datas
* @param keySize
* @return
*/
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;
}
int offSet = 0;
byte[] buff;
byte[] resultDatas;
int i = 0;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
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;
}
resultDatas = out.toByteArray();
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
}
return resultDatas;
}
}
/**
* JWT工具类
*
* @author mjr
* @date 2024/12/11 15:43
**/
@Slf4j
@Component
public class JwtTokenUtils {
@Resource
private JwtProperties jwtProperties;
/**
* 生成 accessToken(初始化Token令牌)
*
* @param uuid
* @param account 用户账号
* @return
*/
public String createAccessToken(String uuid, String account) throws Exception {
// 当前时间
Date currentDate = new Date();
// Base64 编码的私钥字符串
String privateKeyStr = SecretConstants.SIGN_RSA_PRIVATE_KEY;
// 将 Base64 编码的私钥字符串转换为 PrivateKey 对象
byte[] privateKeyBytes = Base64.decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// 登陆成功生成JWT
return Jwts.builder()
// 放入appId
.setId(uuid)
// 主题
.setSubject(account)
// 签发时间
.setIssuedAt(currentDate)
// 签发者
.setIssuer("mjr")
// 自定义属性 放入用户拥有权限
// .claim("authorities", JSON.toJSONString(activeUser.getAuthorities()))
// 失效时间
.setExpiration(DateUtil.offsetSecond(currentDate, this.jwtProperties.getAccessTokenExpiration()))
// 签名算法和密钥,使用 RS256 算法和私钥签名
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 生成 refreshToken
*
* @param accessToken 原令牌
* @return
*/
public String creatRefreshToken(String accessToken) {
// 当前时间
Date currentDate = new Date();
String refreshedToken;
try {
Claims claims = this.getClaimsFromToken(accessToken);
if (claims == null) {
return null;
}
claims.put(Claims.ISSUED_AT, currentDate);
// Base64 编码的私钥字符串
String privateKeyStr = SecretConstants.SIGN_RSA_PRIVATE_KEY;
// 将 Base64 编码的私钥字符串转换为 PrivateKey 对象
byte[] privateKeyBytes = Base64.decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
refreshedToken = Jwts.builder()
.setClaims(claims)
// .claim("custom", Base64.decodeStr(claims.getId() + claims.getSubject(), StandardCharsets.UTF_8))
// 签发时间
.setIssuedAt(currentDate)
// 失效时间
.setExpiration(DateUtil.offsetSecond(currentDate, this.jwtProperties.getRefreshTokenExpiration()))
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
} catch (Exception e) {
log.error(e.getMessage());
refreshedToken = null;
}
return refreshedToken;
}
/**
* 从token中解析出数据,验证 JWT(使用公钥)
*
* @param token 令牌
* @return
*/
public Claims getClaimsFromToken(String token) {
Claims claims;
try {
// 将 Base64 编码的公钥字符串转换为 PublicKey 对象
byte[] publicKeyBytes = Base64.decode(SecretConstants.SIGN_RSA_PUBLIC_KEY);
PublicKey publicKey = KeyUtil.generatePublicKey("RSA", publicKeyBytes);
claims = Jwts.parser()
// 公钥验签
.setSigningKey(publicKey)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.error(e.getMessage());
claims = null;
}
return claims;
}
/**
* 从令牌中获取 appId
*
* @param token 令牌
* @return 用户名
*/
public String getAppIdFromToken(String token) {
String appId;
try {
Claims claims = this.getClaimsFromToken(token);
if (claims == null) {
return null;
}
appId = claims.getId();
} catch (Exception e) {
log.error(e.getMessage());
appId = null;
}
return appId;
}
/**
* 从令牌中获取用户账号
*
* @param token 令牌
* @return 用户名
*/
public String getAccountFromToken(String token) {
String username;
try {
Claims claims = this.getClaimsFromToken(token);
if (claims == null) {
return null;
}
username = claims.getSubject();
} catch (Exception e) {
log.error(e.getMessage());
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = this.getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 验证令牌是否有效
*
* @param accountParam 账号
* @param token 令牌
* @return
*/
public Boolean validateToken(String accountParam, String token) {
String account = this.getAccountFromToken(token);
return StringUtils.equals(account, accountParam) && !this.isTokenExpired(token);
}
}
application.yml文件也可以直接写到代码里去,这样更灵活
# JWT配置
jwt:
# accessToken过期时间,单位秒 1天后过期=86400 7天后过期=604800
accessTokenExpiration: 60
# refreshToken过期时间,单位秒 1天后过期=86400 7天后过期=604800
refreshTokenExpiration: 604800
# 不需要认证的接口
antMatchers:
- /login
- /user/login
- /login/getCaptcha
- /register