参考原文Security认证流程
第一步:先认识一下令牌
开始断点
执行new UsernamePasswordAuthenticationToken
1.Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。(我们实现类是UsernamePasswordAuthenticationToken)
点击UsernamePasswordAuthenticationToken他继承 AbstractAuthenticationToken
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
AbstractAuthenticationToken 又实现 Authentication
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer
Authentication都是获取令牌的方法
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Represents the token for an authentication request or for an authenticated principal
* once the request has been processed by the
* {@link AuthenticationManager#authenticate(Authentication)} method.
* <p>
* Once the request has been authenticated, the <tt>Authentication</tt> will usually be
* stored in a thread-local <tt>SecurityContext</tt> managed by the
* {@link SecurityContextHolder} by the authentication mechanism which is being used. An
* explicit authentication can be achieved, without using one of Spring Security's
* authentication mechanisms, by creating an <tt>Authentication</tt> instance and using
* the code:
*
* <pre>
* SecurityContext context = SecurityContextHolder.createEmptyContext();
* context.setAuthentication(anAuthentication);
* SecurityContextHolder.setContext(context);
* </pre>
*
* Note that unless the <tt>Authentication</tt> has the <tt>authenticated</tt> property
* set to <tt>true</tt>, it will still be authenticated by any security interceptor (for
* method or web invocations) which encounters it.
* <p>
* In most cases, the framework transparently takes care of managing the security context
* and authentication objects for you.
*
* @author Ben Alex
*/
public interface Authentication extends Principal, Serializable {
/**
* Set by an <code>AuthenticationManager</code> to indicate the authorities that the
* principal has been granted. Note that classes should not rely on this value as
* being valid unless it has been set by a trusted <code>AuthenticationManager</code>.
* <p>
* Implementations should ensure that modifications to the returned collection array
* do not affect the state of the Authentication object, or use an unmodifiable
* instance.
* </p>
* @return the authorities granted to the principal, or an empty collection if the
* token has not been authenticated. Never null.
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* The credentials that prove the principal is correct. This is usually a password,
* but could be anything relevant to the <code>AuthenticationManager</code>. Callers
* are expected to populate the credentials.
* @return the credentials that prove the identity of the <code>Principal</code>
*/
Object getCredentials();
/**
* Stores additional details about the authentication request. These might be an IP
* address, certificate serial number etc.
* @return additional details about the authentication request, or <code>null</code>
* if not used
*/
Object getDetails();
/**
* The identity of the principal being authenticated. In the case of an authentication
* request with username and password, this would be the username. Callers are
* expected to populate the principal for an authentication request.
* <p>
* The <tt>AuthenticationManager</tt> implementation will often return an
* <tt>Authentication</tt> containing richer information as the principal for use by
* the application. Many of the authentication providers will create a
* {@code UserDetails} object as the principal.
* @return the <code>Principal</code> being authenticated or the authenticated
* principal after authentication.
*/
Object getPrincipal();
/**
* Used to indicate to {@code AbstractSecurityInterceptor} whether it should present
* the authentication token to the <code>AuthenticationManager</code>. Typically an
* <code>AuthenticationManager</code> (or, more often, one of its
* <code>AuthenticationProvider</code>s) will return an immutable authentication token
* after successful authentication, in which case that token can safely return
* <code>true</code> to this method. Returning <code>true</code> will improve
* performance, as calling the <code>AuthenticationManager</code> for every request
* will no longer be necessary.
* <p>
* For security reasons, implementations of this interface should be very careful
* about returning <code>true</code> from this method unless they are either
* immutable, or have some way of ensuring the properties have not been changed since
* original creation.
* @return true if the token has been authenticated and the
* <code>AbstractSecurityInterceptor</code> does not need to present the token to the
* <code>AuthenticationManager</code> again for re-authentication.
*/
boolean isAuthenticated();
/**
* See {@link #isAuthenticated()} for a full description.
* <p>
* Implementations should <b>always</b> allow this method to be called with a
* <code>false</code> parameter, as this is used by various classes to specify the
* authentication token should not be trusted. If an implementation wishes to reject
* an invocation with a <code>true</code> parameter (which would indicate the
* authentication token is trusted - a potential security risk) the implementation
* should throw an {@link IllegalArgumentException}.
* @param isAuthenticated <code>true</code> if the token should be trusted (which may
* result in an exception) or <code>false</code> if the token should not be trusted
* @throws IllegalArgumentException if an attempt to make the authentication token
* trusted (by passing <code>true</code> as the argument) is rejected due to the
* implementation being immutable or implementing its own alternative approach to
* {@link #isAuthenticated()}
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
第二步认证
AuthenticationManager接口:定义了认证Authentication的方法
ProviderManager实现这个接口
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean
断点进入就是ProviderManager的public Authentication authenticate(Authentication authentication)方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
AuthenticationManager都会包含多个AuthenticationProvider对象,有任何一个AuthenticationProvider验证通过,都属于认证通过。
//AuthenticationManager类下面有这个属性(至今不知道这个providers从哪里来的)
private List<AuthenticationProvider> providers = Collections.emptyList();
断点进来发现是AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider的authenticate方法
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
在执行AbstractUserDetailsAuthenticationProvider的authenticate方法里面进入本身的retrieveUser抽象方法
而retrieveUser具体实现是DaoAuthenticationProvider(DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider)
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);调用的UserDetailsService.loadUserByUsername()方法,我们实现该方法即可实现secutiry的校验
断点进入
我们自己创建一个userDetailsService服务并实现loadUserByUsername
因为返回的是UserDetails类型,项目返回JwtUserDto但是是实现了UserDetails,除了实现UserDetails,项目JwtUserDto加了dataScopes,authorities,还有getRoles
/*
* Copyright (c) 2024 南京瑞玥科技有限公司. All Rights Reserved.
*/
package com.njry.modules.security.service.dto;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Getter;
import com.njry.modules.system.domain.User;
import lombok.Setter;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author wj
* @date 2024-05-11
*/
@Getter
@Setter
@AllArgsConstructor
public class JwtUserDto implements UserDetails {
private final User user;
private final List<Long> dataScopes;
private final List<AuthorityDto> authorities;
public Set<String> getRoles() {
return authorities.stream().map(AuthorityDto::getAuthority).collect(Collectors.toSet());
}
@Override
@JSONField(serialize = false)
public String getPassword() {
return user.getPassword();
}
@Override
@JSONField(serialize = false)
public String getUsername() {
return user.getUsername();
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
@JSONField(serialize = false)
public boolean isEnabled() {
return user.getEnabled();
}
}
/*
* Copyright (c) 2024 南京瑞玥科技有限公司. All Rights Reserved.
*/
package com.njry.modules.security.service;
import com.njry.exception.BadRequestException;
import com.njry.exception.EntityNotFoundException;
import com.njry.modules.security.service.dto.JwtUserDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.njry.modules.system.domain.User;
import com.njry.modules.system.service.DataService;
import com.njry.modules.system.service.RoleService;
import com.njry.modules.system.service.UserService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author njry
* @date 2024-05-11
*/
@Slf4j
@RequiredArgsConstructor
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
private final RoleService roleService;
private final DataService dataService;
private final UserCacheManager userCacheManager;
@Override
public JwtUserDto loadUserByUsername(String username) {
// 先从缓存中找是否登录
JwtUserDto jwtUserDto = userCacheManager.getUserCache(username);
if(jwtUserDto == null){
User user;
try {
user = userService.getLoginData(username);
// user = userService.findByOperId(username);
} catch (EntityNotFoundException e) {
// SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException
throw new UsernameNotFoundException(username, e);
}
if (user == null) {
throw new UsernameNotFoundException("");
} else {
if (!user.getEnabled()) {
throw new BadRequestException("账号未激活!");
}
jwtUserDto = new JwtUserDto(
user,
dataService.getDeptIds(user),
roleService.mapToGrantedAuthorities(user)
);
// 添加缓存数据
userCacheManager.addUserCache(username, jwtUserDto);
}
}
return jwtUserDto;
}
}
最终返回的jwtUserDto
断点继续
中间什么check我都没看
执行自己类的createSuccessAuthentication
看入参
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
继续执行
第三步生成token(Jwt)
具体jwt怎么生成先不研究了,看下面工具类(有涉及redis和SecurityProperties)
redis不贴了,SecurityProperties贴一下
/*
* Copyright (c) 2024 南京瑞玥科技有限公司. All Rights Reserved.
*/
package com.njry.modules.security.security;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import com.njry.modules.security.config.bean.SecurityProperties;
import com.njry.utils.RedisUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author /
*/
@Slf4j
@Component
public class TokenProvider implements InitializingBean {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
public static final String AUTHORITIES_KEY = "user";
private JwtParser jwtParser;
private JwtBuilder jwtBuilder;
public TokenProvider(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties;
this.redisUtils = redisUtils;
}
@Override
public void afterPropertiesSet() {
byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret());
Key key = Keys.hmacShaKeyFor(keyBytes);
jwtParser = Jwts.parserBuilder()
.setSigningKey(key)
.build();
jwtBuilder = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS512);
}
/**
* 创建Token 设置永不过期,
* Token 的时间有效性转到Redis 维护
*
* @param authentication /
* @return /
*/
public String createToken(Authentication authentication) {
return jwtBuilder
// 加入ID确保生成的 Token 都不一致
.setId(IdUtil.simpleUUID())
.claim(AUTHORITIES_KEY, authentication.getName())
.setSubject(authentication.getName())
.compact();
}
/**
* 依据Token 获取鉴权信息
*
* @param token /
* @return /
*/
Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
User principal = new User(claims.getSubject(), "******", new ArrayList<>());
return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
}
public Claims getClaims(String token) {
return jwtParser
.parseClaimsJws(token)
.getBody();
}
/**
* @param token 需要检查的token
*/
public void checkRenewal(String token) {
// 判断是否续期token,计算token的过期时间
String loginKey = loginKey(token);
long time = redisUtils.getExpire(loginKey) * 1000;
Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time);
// 判断当前时间与过期时间的时间差
long differ = expireDate.getTime() - System.currentTimeMillis();
// 如果在续期检查的范围内,则续期
if (differ <= properties.getDetect()) {
long renew = time + properties.getRenew();
redisUtils.expire(loginKey, renew, TimeUnit.MILLISECONDS);
}
}
public String getToken(HttpServletRequest request) {
final String requestHeader = request.getHeader(properties.getHeader());
if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) {
return requestHeader.substring(7);
}
return null;
}
/**
* 获取登录用户RedisKey
* @param token /
* @return key
*/
public String loginKey(String token) {
Claims claims = getClaims(token);
String md5Token = DigestUtil.md5Hex(token);
return properties.getOnlineKey() + claims.getSubject() + "-" + md5Token;
}
}
/*
* Copyright (c) 2024 南京瑞玥科技有限公司. All Rights Reserved.
*/
package com.njry.modules.security.config.bean;
import lombok.Data;
import java.util.Set;
/**
* Jwt参数配置
*
* @author njry
* @date 2024-05-11
*/
@Data
public class SecurityProperties {
/**
* Request Headers : Authorization
*/
private String header;
/**
* 令牌前缀,最后留个空格 Bearer
*/
private String tokenStartWith;
/**
* 必须使用最少88位的Base64对该令牌进行编码
*/
private String base64Secret;
/**
* 令牌过期时间 此处单位/毫秒
*/
private Long tokenValidityInSeconds;
/**
* 在线用户 key,根据 key 查询 redis 中在线用户的数据
*/
private String onlineKey;
/**
* 验证码 key
*/
private String codeKey;
/**
* token 续期检查
*/
private Long detect;
/**
* 续期时间
*/
private Long renew;
/**
* reqTime : 600000
*/
private int reqTime;
/**
* reqTime : 600000
*/
private Set<String> unFilterUrl;
public String getTokenStartWith() {
return tokenStartWith + " ";
}
}