在现代企业级应用中,用户需要访问多个相关但独立的系统。传统的每次访问都需要重新登录的方式不仅用户体验差,而且安全性也难以保障。本文将深入探讨基于Spring Security的单点登录(SSO)和自动登录机制的实现原理。
单点登录是指用户只需要登录一次,就可以访问所有相互信任的应用系统。
自动登录是指用户在一定时间内无需重复输入用户名密码即可自动完成身份认证。
让我们先分析一下提供的代码片段:
// 1. 手动查询用户
SysUser sysUser = userService.selectUserByUserName(username);
if (sysUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 3. 查询权限
Set<String> permissions = sysPermissionService.getMenuPermission(sysUser);
// 4. 构造LoginUser对象
LoginUser loginUser = new LoginUser(sysUser.getUserId(),sysUser.getDeptId(),sysUser, permissions);
// 4. 构造已认证的Authentication对象
authentication = new UsernamePasswordAuthenticationToken(
loginUser, // principal - 这里传递的是完整的LoginUser对象
null, // credentials
loginUser.getAuthorities() // authorities
);
// 5. 设置到Security上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
Long userId = SecurityUtils.getUserId();
这段代码展示了手动构建认证信息的核心流程。
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(LoginUser loginUser) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(loginUser.getUsername())
.claim("userId", loginUser.getUserId())
.claim("permissions", loginUser.getPermissions())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String validateTokenAndGetUserId(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.get("userId", String.class);
}
}
@Component
public class SsoAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
try {
String userId = tokenProvider.validateTokenAndGetUserId(token);
SysUser sysUser = userService.selectUserById(userId);
if (sysUser != null) {
Set<String> permissions = sysPermissionService.getMenuPermission(sysUser);
LoginUser loginUser = new LoginUser(sysUser.getUserId(),
sysUser.getDeptId(),
sysUser,
permissions);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
loginUser,
null,
loginUser.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.and()
.rememberMe()
.rememberMeParameter("remember-me")
.tokenRepository(persistentTokenRepository)
.tokenValiditySeconds(86400) // 24小时
.userDetailsService(userDetailsService);
}
}
@Component
public class PersistentTokenRepositoryImpl implements PersistentTokenRepository {
@Autowired
private RememberMeTokenMapper rememberMeTokenMapper;
@Override
public void createNewToken(PersistentRememberMeToken token) {
RememberMeToken entity = new RememberMeToken();
entity.setSeries(token.getSeries());
entity.setUsername(token.getUsername());
entity.setToken(token.getTokenValue());
entity.setLastUsed(token.getDate());
rememberMeTokenMapper.insert(entity);
}
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
RememberMeToken entity = new RememberMeToken();
entity.setSeries(series);
entity.setToken(tokenValue);
entity.setLastUsed(lastUsed);
rememberMeTokenMapper.updateByPrimaryKey(entity);
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
RememberMeToken entity = rememberMeTokenMapper.selectByPrimaryKey(seriesId);
if (entity != null) {
return new PersistentRememberMeToken(
entity.getUsername(),
entity.getSeries(),
entity.getToken(),
entity.getLastUsed()
);
}
return null;
}
@Override
public void removeUserTokens(String username) {
rememberMeTokenMapper.deleteByUsername(username);
}
}
@Service
public class SysLoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserService userService;
@Autowired
private SysPermissionService sysPermissionService;
/**
* 用户登录
*/
public String login(String username, String password, String code, String uuid) {
// 1. 验证码校验
validateCaptcha(code, uuid);
// 2. 用户认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
// 3. 认证成功后生成JWT Token
SecurityContextHolder.getContext().setAuthentication(authentication);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return tokenProvider.generateToken(loginUser);
}
/**
* 自动登录处理
*/
public String autoLogin(String token) {
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
String userId = tokenProvider.validateTokenAndGetUserId(token);
SysUser sysUser = userService.selectUserById(userId);
if (sysUser != null) {
Set<String> permissions = sysPermissionService.getMenuPermission(sysUser);
LoginUser loginUser = new LoginUser(sysUser.getUserId(),
sysUser.getDeptId(),
sysUser,
permissions);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
loginUser,
null,
loginUser.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成新的token
return tokenProvider.generateToken(loginUser);
}
}
throw new AuthenticationException("自动登录失败");
}
private void validateCaptcha(String code, String uuid) {
// 验证码校验逻辑
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null || !code.equalsIgnoreCase(captcha)) {
throw new CaptchaException("验证码错误");
}
}
}
public class SecurityUtils {
/**
* 获取用户ID
*/
public static Long getUserId() {
try {
return getLoginUser().getUserId();
} catch (Exception e) {
throw new CustomException("获取用户ID异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取登录用户信息
*/
public static LoginUser getLoginUser() {
try {
return (LoginUser) getAuthentication().getPrincipal();
} catch (Exception e) {
throw new CustomException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取Authentication
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
@Component
public class LoginLogAspect {
@Around("execution(* com.example.service.SysLoginService.login(..))")
public Object logLogin(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
// 记录成功日志
logLoginSuccess(joinPoint.getArgs());
return result;
} catch (Exception e) {
// 记录失败日志
logLoginFailure(joinPoint.getArgs(), e);
throw e;
} finally {
long endTime = System.currentTimeMillis();
logger.info("登录耗时: {}ms", endTime - startTime);
}
}
}
通过本文的介绍,我们了解了:
在实际项目中,需要根据业务需求选择合适的方案,并注意安全性和性能的平衡。单点登录和自动登录机制的合理运用,能够显著提升用户体验和系统安全性。