1. 若依框架免密登录的实现原理
若依框架作为国内流行的开源后台管理系统,其免密登录功能主要基于Token机制实现。核心流程是当用户首次登录后,系统生成一个加密的Token并存储在客户端(通常是浏览器的LocalStorage或Cookie中),后续请求携带该Token即可完成身份验证。
1.1 JWT Token的工作机制
若依默认采用JWT(JSON Web Token)作为免密登录的实现方式。一个典型的JWT Token由三部分组成:
code复制Header.Payload.Signature
Header部分包含算法类型和token类型:
json复制{
"alg": "HS256",
"typ": "JWT"
}
Payload部分包含用户关键信息:
json复制{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Signature部分用于验证消息完整性:
code复制HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
重要提示:若依默认的JWT密钥存储在application.yml中,生产环境必须修改默认值,否则会带来严重安全隐患。
1.2 免密登录的时序流程
- 用户首次输入账号密码登录
- 服务端验证通过后生成JWT Token
- Token通过响应头或响应体返回前端
- 前端存储Token(通常使用localStorage.setItem())
- 后续请求在Authorization头携带Token
- 服务端Filter校验Token有效性
- 校验通过后允许访问受保护资源
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 外部系统无感跳转的实现方案
2.1 基于Token共享的SSO实现
要实现外部系统无感跳转,核心是建立信任关系。常见方案有:
- 父系统Token传递:
javascript复制// 父系统跳转代码
window.location.href = `http://sub-system.com?token=${getToken()}`;
- 跨域PostMessage通信:
javascript复制// 父系统
window.open('http://sub-system.com');
window.addEventListener('message', (event) => {
if (event.origin === 'http://sub-system.com') {
event.source.postMessage({token: getToken()}, event.origin);
}
});
// 子系统
window.opener.postMessage('request_token', '*');
window.addEventListener('message', (event) => {
if (event.data.token) {
localStorage.setItem('token', event.data.token);
}
});
- 后端Token验证接口:
java复制@GetMapping("/verifyToken")
public R verifyToken(String token) {
try {
Claims claims = JwtUtils.parseToken(token);
return R.success(claims.getSubject());
} catch (Exception e) {
return R.error("无效token");
}
}
2.2 安全性增强措施
- Token有效期控制:
yaml复制# application.yml
token:
expireTime: 1800 # 30分钟过期
refreshTime: 300 # 最后5分钟可刷新
- IP绑定验证:
java复制String ip = ServletUtils.getClientIP(request);
if (!ip.equals(claims.get("ip", String.class))) {
throw new RuntimeException("IP变更需重新登录");
}
- 关键操作二次验证:
vue复制<template>
<el-dialog title="敏感操作验证" :visible.sync="showAuth">
<el-input v-model="password" type="password"></el-input>
<el-button @click="confirmAuth">验证</el-button>
</el-dialog>
</template>
3. 若依框架的具体配置步骤
3.1 后端配置调整
- 修改Token配置:
yaml复制# ruoyi-admin/src/main/resources/application.yml
token:
header: Authorization
secret: your_strong_secret_here # 必须修改
expireTime: 7200 # 2小时
- 添加跨域支持:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("Authorization");
}
}
- 自定义Token过滤器:
java复制public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
String token = request.getHeader(tokenHeader);
if (StringUtils.isNotEmpty(token)) {
Claims claims = JwtUtils.parseToken(token);
String username = claims.getSubject();
// 将用户信息存入SecurityContext
}
chain.doFilter(request, response);
}
}
3.2 前端适配改造
- 请求拦截器配置:
javascript复制// src/utils/request.js
service.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = token;
}
return config;
});
- 自动刷新Token逻辑:
javascript复制// src/api/login.js
export function refreshToken() {
return request({
url: '/auth/refresh',
method: 'post',
data: { token: localStorage.getItem('token') }
});
}
// 响应拦截器中处理
service.interceptors.response.use(response => {
if (response.data.code === 401 && response.config.url !== '/login') {
return refreshToken().then(res => {
localStorage.setItem('token', res.token);
// 重试原请求
return service(response.config);
});
}
return response;
});
4. 实际应用中的问题排查
4.1 常见问题及解决方案
- Token失效但页面仍可操作
- 现象:后端返回401但前端路由未跳转登录页
- 解决方案:
javascript复制// src/permission.js
router.beforeEach((to, from, next) => {
if (hasToken()) {
if (isTokenExpired()) {
next('/login');
} else {
next();
}
} else {
next('/login');
}
});
- 跨域携带Cookie失败
- 现象:Chrome浏览器无法发送跨域Cookie
- 解决方案:
java复制// 后端配置
response.setHeader("Access-Control-Allow-Credentials", "true");
javascript复制// 前端axios配置
axios.defaults.withCredentials = true;
- 多标签页登录冲突
- 现象:一个标签页退出登录后其他标签页仍可操作
- 解决方案:
javascript复制// 监听storage事件
window.addEventListener('storage', (event) => {
if (event.key === 'token' && !event.newValue) {
router.push('/login');
}
});
4.2 性能优化建议
- Token压缩方案:
java复制// 使用更紧凑的claim结构
Claims claims = Jwts.claims().setSubject(user.getUserId());
claims.put("r", user.getRoles().stream().map(Role::getRoleKey).collect(Collectors.joining(",")));
- Redis缓存用户信息:
java复制@Cacheable(value = "userDetails", key = "#username")
public UserDetails loadUserByUsername(String username) {
// 数据库查询
}
- 前端请求合并:
javascript复制// 使用vue-query等库合并重复请求
const { data } = useQuery('userInfo', fetchUserInfo);
5. 企业级扩展方案
5.1 微服务架构下的实现
- 网关统一鉴权:
java复制// 网关过滤器
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (JwtUtils.validateToken(token)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
- 服务间Token传递:
yaml复制# feign配置
feign:
client:
config:
default:
requestInterceptors:
- com.example.FeignAuthInterceptor
5.2 多因素认证集成
- 短信验证码集成:
java复制@PostMapping("/loginWithSms")
public R loginWithSms(@RequestBody LoginBody loginBody) {
String cacheCode = redisCache.getCacheObject(loginBody.getPhone());
if (!loginBody.getCode().equals(cacheCode)) {
return R.error("验证码错误");
}
// 后续Token生成逻辑
}
- 生物识别支持:
javascript复制// 调用WebAuthn API
navigator.credentials.get({
publicKey: {
challenge: new Uint8Array([...]),
rpId: 'your-domain.com',
userVerification: 'required'
}
}).then(assertion => {
// 发送assertion到后端验证
});
6. 监控与安全审计
6.1 登录行为监控
- 日志记录配置:
java复制@Aspect
@Component
public class LoginLogAspect {
@AfterReturning(pointcut = "execution(* com.ruoyi.web.controller.system.SysLoginController.login(..))",
returning = "result")
public void afterLogin(JoinPoint joinPoint, Object result) {
R r = (R) result;
if (r.getCode() == 200) {
LoginBody loginBody = (LoginBody) joinPoint.getArgs()[0];
// 记录登录日志
}
}
}
- 异常登录检测:
sql复制-- 检测短时间内多次失败登录
SELECT COUNT(*) FROM sys_login_log
WHERE status = 1 AND user_name = ? AND login_time > DATE_SUB(NOW(), INTERVAL 5 MINUTE);
6.2 安全防护措施
- Token盗用防护:
java复制// 生成指纹
String fingerprint = DigestUtils.md5DigestAsHex(
(request.getHeader("User-Agent") +
ServletUtils.getClientIP(request)).getBytes());
// 存入Token
claims.put("fp", fingerprint);
- 关键操作日志:
java复制@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PostMapping("/updateUser")
public R updateUser(@RequestBody SysUser user) {
// 业务逻辑
}
在实际项目中,我们曾遇到一个典型场景:某企业需要将若依系统与第三方HR系统对接,要求HR系统登录后无需再次认证即可进入若依。通过实现Token共享方案,最终达到了300ms内完成跨系统跳转的体验,用户完全感知不到认证过程。关键点在于:
- 两个系统约定统一的JWT加密密钥
- 在跳转URL中添加一次性验证参数
- 若依侧添加专门的白名单验证过滤器
- 实现Token自动续期机制
这种深度集成方案需要特别注意安全边界控制,建议:
- 限制Token的有效期(不超过8小时)
- 记录完整的跳转审计日志
- 对敏感操作保持二次验证
- 定期轮换加密密钥
