一、传统Session认证授权的底层逻辑
1.1 什么是Session认证
Session认证是基于HTTP无状态特性的解决方案,核心是服务端保存用户会话状态,客户端通过Cookie携带SessionID进行身份关联。其本质是服务端状态存储+客户端身份标识传递的组合模式。
编辑
1.2 Session认证的核心流程
- 用户提交用户名密码到服务端
- 服务端验证通过后创建Session,存储用户信息
- 生成唯一SessionID,通过Cookie返回给客户端
- 后续请求客户端携带SessionID,服务端查询Session完成身份认证
1.3 授权机制与Session的结合
授权是在认证基础上对用户操作权限的控制,传统模式下:
- 认证成功后,将用户权限信息存入Session
- 每次请求时从Session获取权限信息进行校验
- 通过Filter/Interceptor实现权限拦截
二、单体应用中的Session实现
2.1 技术选型与环境配置
pom.xml核心依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>session-auth-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<fastjson2.version>2.0.32</fastjson2.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/session_auth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password:
database: 0
session:
store-type: redis
timeout: 1800
server:
port: 8080
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.jam.demo.entity
2.2 数据库设计
用户表:
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码(加密)',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`status` tinyint DEFAULT '1' COMMENT '状态(1:正常,0:禁用)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
`role_code` varchar(50) NOT NULL COMMENT '角色编码',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `sys_user_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
CREATE TABLE `sys_permission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`perm_name` varchar(50) NOT NULL COMMENT '权限名称',
`perm_code` varchar(50) NOT NULL COMMENT '权限编码',
`url` varchar(200) DEFAULT NULL COMMENT '资源路径',
`method` varchar(10) DEFAULT NULL COMMENT '请求方法',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_perm_code` (`perm_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';
CREATE TABLE `sys_role_permission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`perm_id` bigint NOT NULL COMMENT '权限ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_perm` (`role_id`,`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';
2.3 核心代码实现
用户实体类:
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体
* @author ken
*/
@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String nickname;
private String email;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
用户DTO:
package com.jam.demo.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 登录请求DTO
* @author ken
*/
@Data
@ApiModel("登录请求参数")
public class LoginDTO {
@NotBlank(message = "用户名不能为空")
@ApiModelProperty(value = "用户名", required = true)
private String username;
@NotBlank(message = "密码不能为空")
@ApiModelProperty(value = "密码", required = true)
private String password;
}
用户Mapper:
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SysUser;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 用户Mapper接口
* @author ken
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户信息
*/
@Select("SELECT * FROM sys_user WHERE username = #{username}")
SysUser selectByUsername(@Param("username") String username);
/**
* 查询用户角色编码
* @param userId 用户ID
* @return 角色编码列表
*/
@Select("SELECT r.role_code FROM sys_user_role ur " +
"LEFT JOIN sys_role r ON ur.role_id = r.id " +
"WHERE ur.user_id = #{userId}")
List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
/**
* 查询用户权限编码
* @param userId 用户ID
* @return 权限编码列表
*/
@Select("SELECT p.perm_code FROM sys_user_role ur " +
"LEFT JOIN sys_role_permission rp ON ur.role_id = rp.role_id " +
"LEFT JOIN sys_permission p ON rp.perm_id = p.id " +
"WHERE ur.user_id = #{userId}")
List<String> selectPermCodesByUserId(@Param("userId") Long userId);
}
用户Service:
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.SysUser;
import com.jam.demo.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysUserService extends ServiceImpl<SysUserMapper, SysUser> implements UserDetailsService {
private final SysUserMapper sysUserMapper;
private final PasswordEncoder passwordEncoder;
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return 用户详情
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!StringUtils.hasText(username)) {
throw new UsernameNotFoundException("用户名不能为空");
}
// 查询用户基本信息
SysUser sysUser = sysUserMapper.selectByUsername(username);
if (sysUser == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 查询用户角色
List<String> roleCodes = sysUserMapper.selectRoleCodesByUserId(sysUser.getId());
// 查询用户权限
List<String> permCodes = sysUserMapper.selectPermCodesByUserId(sysUser.getId());
// 构建权限列表(角色+权限)
List<SimpleGrantedAuthority> authorities = Lists.newArrayList();
authorities.addAll(roleCodes.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList()));
authorities.addAll(permCodes.stream()
.map(perm -> new SimpleGrantedAuthority(perm))
.collect(Collectors.toList()));
return User.builder()
.username(sysUser.getUsername())
.password(sysUser.getPassword())
.authorities(authorities)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(sysUser.getStatus() == 0)
.build();
}
/**
* 用户注册
* @param username 用户名
* @param password 密码
* @param nickname 昵称
* @return 注册结果
*/
public boolean register(String username, String password, String nickname) {
if (sysUserMapper.selectByUsername(username) != null) {
return false;
}
SysUser user = new SysUser();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setNickname(StringUtils.hasText(nickname) ? nickname : username);
user.setStatus(1);
return save(user);
}
}
认证控制器:
package com.jam.demo.controller;
import com.jam.demo.dto.LoginDTO;
import com.jam.demo.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
/**
* 认证控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Api(tags = "认证接口")
public class AuthController {
private final SysUserService sysUserService;
/**
* 用户登录
* @param loginDTO 登录参数
* @param session HTTP会话
* @return 登录结果
*/
@PostMapping("/login")
@ApiOperation("用户登录")
public ResponseEntity<?> login(@Validated @RequestBody LoginDTO loginDTO, HttpSession session) {
// Spring Security会自动处理认证,这里直接返回成功信息
Map<String, Object> result = Maps.newHashMap();
result.put("code", 200);
result.put("message", "登录成功");
result.put("sessionId", session.getId());
result.put("timeout", session.getMaxInactiveInterval());
return ResponseEntity.ok(result);
}
/**
* 用户注册
* @param username 用户名
* @param password 密码
* @param nickname 昵称
* @return 注册结果
*/
@PostMapping("/register")
@ApiOperation("用户注册")
public ResponseEntity<?> register(
@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) String nickname) {
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return ResponseEntity.badRequest().body("用户名和密码不能为空");
}
boolean success = sysUserService.register(username, password, nickname);
if (success) {
return ResponseEntity.ok("注册成功");
} else {
return ResponseEntity.badRequest().body("用户名已存在");
}
}
/**
* 用户登出
* @param request 请求对象
* @param response 响应对象
* @return 登出结果
*/
@GetMapping("/logout")
@ApiOperation("用户登出")
public ResponseEntity<?> logout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return ResponseEntity.ok("登出成功");
}
/**
* 获取当前用户信息
* @param session HTTP会话
* @return 用户信息
*/
@GetMapping("/current-user")
@ApiOperation("获取当前用户信息")
public ResponseEntity<?> getCurrentUser(HttpSession session) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.getPrincipal().equals("anonymousUser")) {
return ResponseEntity.status(401).body("未登录");
}
Map<String, Object> userInfo = Maps.newHashMap();
userInfo.put("username", auth.getName());
userInfo.put("authorities", auth.getAuthorities());
userInfo.put("sessionId", session.getId());
return ResponseEntity.ok(userInfo);
}
}
资源控制器:
package com.jam.demo.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 资源控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/resources")
@Api(tags = "资源接口")
public class ResourceController {
@GetMapping("/public")
@ApiOperation("公共资源")
public String publicResource() {
return "这是公共资源,无需认证即可访问";
}
@GetMapping("/user")
@ApiOperation("用户资源")
@PreAuthorize("hasRole('USER')")
public String userResource() {
return "这是用户资源,需要USER角色";
}
@GetMapping("/admin")
@ApiOperation("管理员资源")
@PreAuthorize("hasRole('ADMIN')")
public String adminResource() {
return "这是管理员资源,需要ADMIN角色";
}
@GetMapping("/permission")
@ApiOperation("特定权限资源")
@PreAuthorize("hasAuthority('sys:user:view')")
public String permissionResource() {
return "这是需要特定权限的资源";
}
}
Security配置:
package com.jam.demo.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置
* @author ken
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 安全过滤链配置
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/auth/register", "/resources/public",
"/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginProcessingUrl("/auth/login")
.successHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"message\":\"登录成功\"}");
})
.failureHandler((request, response, exception) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":500,\"message\":\"" + exception.getMessage() + "\"}");
})
)
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"message\":\"登出成功\"}");
})
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/auth/login?invalid=true")
.maximumSessions(1)
.expiredUrl("/auth/login?expired=true")
);
return http.build();
}
}
三、微服务架构下的Session共享问题
3.1 微服务Session挑战
在微服务架构中,传统单机Session面临以下问题:
- Session不共享:用户请求分发到不同服务实例导致认证失效
- Session同步复杂:多实例间Session同步成本高
- 跨服务认证:不同微服务间需要统一的身份认证
- 扩展性差:Session存储在应用内存,限制服务实例扩展
3.2 Session共享解决方案
3.2.1 基于Redis的Session共享
原理:将Session存储在Redis中,所有服务实例共享Redis中的Session数据
实现方案:
package com.jam.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Redis Session配置
* @author ken
*/
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
/**
* Cookie序列化配置
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setUseHttpOnlyCookie(true);
serializer.setSameSite("Lax");
serializer.setUseSecureCookie(false); // 生产环境建议设为true
return serializer;
}
}
3.2.2 Session粘滞(IP Hash)
负载均衡器根据用户IP地址计算哈希,确保同一用户请求始终分发到同一服务实例。优点是实现简单,缺点是不利于服务扩缩容,单点故障影响用户体验。
3.2.3 Token认证替代方案
JWT(JSON Web Token)作为无状态认证方案,客户端存储Token,服务端无需保存会话状态,天然适合微服务架构。
四、Session安全加固
4.1 Session安全风险
- Session劫持:通过窃取SessionID冒充用户身份
- Session固定:攻击者诱导用户使用固定SessionID
- CSRF攻击:利用用户已认证的Session发起恶意请求
- Session超时设置不当:Session有效期过长增加被盗风险
4.2 安全加固措施
4.2.1 SessionID保护
package com.jam.demo.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* Session安全过滤器
* @author ken
*/
@Slf4j
@Component
public class SessionSecurityFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 设置Cookie的HttpOnly和Secure属性
res.setHeader("Set-Cookie", "SESSIONID=" + req.getSession().getId() +
"; HttpOnly; Secure; SameSite=Lax; Path=/");
// 验证请求来源
String referer = req.getHeader("Referer");
String host = req.getServerName();
if (referer != null && !referer.contains(host)) {
log.warn("可疑的请求来源: {}", referer);
// 可以根据需要进行拦截处理
}
// 检查Session是否包含用户信息
HttpSession session = req.getSession(false);
if (session != null && session.getAttribute("USER_INFO") == null) {
log.warn("Session中用户信息缺失,可能存在Session劫持风险");
}
chain.doFilter(request, response);
}
}
4.2.2 敏感操作二次验证
package com.jam.demo.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
* 账户安全控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/account")
@RequiredArgsConstructor
@Api(tags = "账户安全接口")
public class AccountSecurityController {
/**
* 修改密码(需要二次验证)
*/
@PostMapping("/change-password")
@ApiOperation("修改密码")
public String changePassword(
@RequestParam String newPassword,
@RequestParam String verifyCode,
HttpSession session) {
// 验证验证码
String savedCode = (String) session.getAttribute("VERIFY_CODE");
if (savedCode == null || !savedCode.equals(verifyCode)) {
return "验证码错误";
}
// 执行密码修改逻辑
log.info("密码修改成功,更新Session信息");
// 修改密码后使当前Session失效,强制重新登录
session.invalidate();
return "密码修改成功,请重新登录";
}
}
4.2.3 Session超时管理
package com.jam.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.SessionRepositoryFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* Session超时过滤器
* @author ken
*/
@Configuration
public class SessionTimeoutFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 检查Session是否即将过期
HttpSession session = req.getSession(false);
if (session != null) {
int maxInactive = session.getMaxInactiveInterval();
long lastAccessed = session.getLastAccessedTime();
long current = System.currentTimeMillis();
// 如果剩余时间小于60秒,返回警告信息
if ((current - lastAccessed) / 1000 > maxInactive - 60) {
res.setHeader("X-Session-Timeout-Warning", "true");
}
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
五、性能优化与监控
5.1 Session存储优化
Redis缓存优化配置:
spring:
redis:
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
timeout: 2000ms
session:
redis:
flush-mode: on_save
namespace: spring:session
5.2 Session监控
package com.jam.demo.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Session监控控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/monitor")
@RequiredArgsConstructor
public class MonitorController {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 获取Session统计信息
*/
@GetMapping("/session-stats")
public Map<String, Object> getSessionStats(HttpServletRequest request) {
Map<String, Object> stats = Maps.newHashMap();
// 获取当前活跃Session数(通过Redis前缀匹配)
Collection<String> keys = redisTemplate.keys("spring:session:sessions:*");
stats.put("active_sessions", keys != null ? keys.size() : 0);
// 当前请求的Session信息
stats.put("current_session_id", request.getSession().getId());
stats.put("current_session_timeout", request.getSession().getMaxInactiveInterval());
// 服务器信息
stats.put("server_port", request.getServerPort());
stats.put("context_path", request.getContextPath());
return stats;
}
}
六、总结与展望
传统Session认证授权机制在单体应用中成熟可靠,但在微服务架构下面临共享和扩展挑战。通过Redis等分布式存储实现Session共享是过渡方案,而JWT等无状态认证更适合云原生微服务架构。
在实际应用中,应根据系统规模和架构特点选择合适的认证方案:
- 小型单体应用:传统Session认证足够满足需求
- 中大型分布式系统:分布式Session或JWT认证
- 云原生微服务:OAuth2.0/OIDC+JWT的认证授权体系
无论选择哪种方案,都必须重视安全加固,防范Session劫持、固定等攻击,确保系统安全性。