Spring Security的核心思想是授权和认证。认证可以访问系统的用户,而授权则是用户可以访问的资源
认证是调用authenticationManager.authenticate()方法来获得证书authentication,一般我们采用用户名、密码方式认证,那么authentication的实现类就是UsernamePasswordAuthentication。
一、Authentication,AuthenticationManager,AuthenticationProvider
Authentication的继承关系大致如下,其中Principal代表用户主体的概念,如用User,login id或者username代表一个entity,主要方法有equals()和getName();
Authentication用于存储身份验证信息,接口内容如下,包括getAuthorities(),getDetails(),getPrinciple(),getCredentials()以及isAuthenticated()。
public interface Authentication extends Principal, Serializable {
/**
* 一般在JPAEntity中继承UserDetail,重写该方法,
* 存储验证信息Authority集合
* GrantedAuthority是个接口,有方法 String getAuthority();
* GrantedAuthority代表授权,a representation of the granted authority,
* getAuthority()是String类型,一般用SimpleGrantedAuthority(role)来实例化
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 返回证明用户身份的证书,一般是用户密码
*/
Object getCredentials();
/**
* 身份验证request额外的细节,如IP地址,和证书序列号
*/
Object getDetails();
/**
* 返回用户身份,一般是继承了UserDetail的Entity
*/
Object getPrincipal();
/**
* 如果是true,则表示token已经验证通过了,无需再调用AuthenticationManager进行验证
*/
boolean isAuthenticated();
/**
* 见isAuthenticated()
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
介绍完了Authentication,再来聊聊AuthenticationManager和AuthenticationProvider,内容如下。
public interface AuthenticationManager {
/**
* 验证传入的authentication信息
* 验证成功则返回一个包含authorities,并设置isAuthorized=true的完整Authentication
* 验证失败则抛出异常
* @param authentication the authentication request object
*
* @return a fully authenticated object including credentials
*
* @throws AuthenticationException if authentication fails
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
public interface AuthenticationProvider {
/**
* 和AuthenticationManager中的authenticate方法一致
* @throws AuthenticationException if authentication fails.
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
/**
* 判断AuthenticationProvider是否支持authentication的补全
*/
boolean supports(Class<?> authentication);
}
二、UserDetail,UserDetailsService,UserCache,User
- UserDetail提供基本用户信息,如getUserName(),getPassword(),List<? extends GrantedAuthority> getAuthorities()等,但一般不会直接调用UserDetail,而是包装后放到Authentication中的principle和authorities中。一般用户实体类继承UserDetail。
- UserDetailsService就简单了,只有一个方法,通过用户名找用户信息UserDetail。
public interface UserDetailsService {
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
- UserCache可以代替UserDetailsService获得用户基本信息UserDetail,如果有缓存则不用每次都执行loadUserByUsername,只需从UserCache中调用UserDetail getUserFromCache(String Username)方法即可。如果没有缓存就执行loadUserByUsername,并把获取的UserDetail存入缓存中。
- User继承了UserDetails和CredentialsContainer两个接口,常用于UserDetailsService中返回loadUserByUsername()方法的结果(代替自己定义的User实体),但注意的是,必须每次返回的都是新建的User,因为它不是immutable。CredentialsContainer用于清除敏感信息。
User包含属性和方法如下,
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
// 继承自CredentialsContainer,继承该方法的类如Authentication需要把敏感信息credential给去除掉
public void eraseCredentials() {
password = null;
}
二、Spring Security执行过程
通过用户名、密码授权,大致过程可归纳为
- 过滤器拦截请求,判断request是否需要验证身份信息
- 将request中的username和password存入UsernamePasswordAuthentication中,用ProviderManager(包含AuthenticationProvider的集合,依次执行多个AuthenticationProvider中的验证方法)的Authenticate方法进行补全优化
- retriveUser(username,authentication)会调用UserDetailsService中的loadUserByUsername()方法,查询获得数据库中的用户基本信息并存入UserDetail的实现类中。
- 获得的resultAuthentication会存入UserCache,且erase掉credentials。
1.AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断request是否为post,是否需要验证身份信息
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 开始验证登录信息
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
...
}
2.UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// request.getParameter("username")
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
3.ProviderManager implements AuthenticationManager ...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
}
...
prepareException(lastException, authentication);
throw lastException;
}
4.AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider ...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 第一次调用是没有userCache的,所以user是null
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 这里调用userDetailsService中的loadUserByUsername(String name)
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 判断user是不是isEnabled(),isAccountNonLocked(),isAccountNonExpired()等
preAuthenticationChecks.check(user);
// 验证authentication(request获取)的密码是否和user(数据库获取)中的一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 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);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// user密码是否过期
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 重新生成了新的UsernamePasswordAuthenticationToken,且isAuthorized=true
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
最终返回的result如下,remove了credentials,存入SecurityContextHolder.getContext().setAuthentication(authResult);
四.OAUTH2工作流程
核心原理还是用authenticationManager的实现类去authenticate(),验证成功后得到token。
先了解些基本概念。
TokenGranter和它的父级,TokenGranter中就一个方法,根据grantType、tokenRequest授予token,返回结果为OAuth2AccessToken。它的实现类实现了不同的生成方法。
public interface TokenGranter {
OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}
步骤如下
1.POSTMAN这里传入的Authorization type 为Basic Auth,则自动用Base64加密后存入名为Authorization的header表头中。
2.BasicAuthenticationFilter位于Spring Security包中,过滤器拦截获取request的表头,解密获得Basic Auth中的Username和Password,接着用authenticationManager.authenticate()获取authentication,并存入SecurityContextHolder中,继续chain.doFilter();
public class BasicAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
String header = request.getHeader("Authorization");
// 如果不是Basic OAuth,则直接到下一个过滤器
if (header == null || !header.startsWith("Basic ")) {
chain.doFilter(request, response);
return;
}
try {
// Base64解码获得tokens ["yuqiyu_home_pc","yuqiyu_secret"]
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String username = tokens[0];
...
if (authenticationIsRequired(username)) {
// 创建UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
// SpringSecurity 验证token
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
// 写入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
}
值得注意的是this.authenticationManager.authenticate(authRequest) 执行过程中会用到userDetailsService.loadUserByUsername()
不同于用户自定义的class继承userDetailsService,这里的loadUserByUsername调用的类是ClientDetailsUserDetailsService,结构如下,返回一个User类,包含用户在@Configuration中自定义的属性
public class ClientDetailsUserDetailsService implements UserDetailsService {
private final ClientDetailsService clientDetailsService;
private String emptyPassword = "";
public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
/**
* @param passwordEncoder the password encoder to set
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.emptyPassword = passwordEncoder.encode("");
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ClientDetails clientDetails;
try {
clientDetails = clientDetailsService.loadClientByClientId(username);
} catch (NoSuchClientException e) {
throw new UsernameNotFoundException(e.getMessage(), e);
}
String clientSecret = clientDetails.getClientSecret();
if (clientSecret== null || clientSecret.trim().length()==0) {
clientSecret = emptyPassword;
}
return new User(username, clientSecret, clientDetails.getAuthorities());
}
}
用户自定义的类如下,其中configure(ClientDetailsServiceConfigurer clients) 方法提供了上面User所需要的信息。
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware {
private static final String ENV_OAUTH = "authentication.oauth.";
private static final String PROP_CLIENTID = "clientid";
private static final String PROP_SECRET = "secret";
private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";
private RelaxedPropertyResolver propertyResolver;
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(propertyResolver.getProperty(PROP_CLIENTID))
.scopes("read", "write")
.authorities(Authorities.ROLE_ADMIN.name(), Authorities.ROLE_USER.name())
.authorizedGrantTypes("password", "refresh_token")
.secret(propertyResolver.getProperty(PROP_SECRET))
.accessTokenValiditySeconds(propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, 1800));
}
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
}
}
3.chain.doFilter()后方法执行到了TokenEndPoint中,这个类位于oauth2的包里,获得了上流传来的Basic OAuth---usernamePasswordAthentication,向下转换成了Principal,该参数如下。parameters则是post中填写的参数,包括username,password和grant_type(password),主要用来和数据库中的数据进行用户名密码比对。
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
...
// tokenGranter进行授权,返回OAuth2AccessToken
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
}
4.执行tokenGranter.grant(grant_type,tokenRequest)方法,方法如下
public final class AuthorizationServerEndpointsConfigurer {
private TokenGranter tokenGranter() {
if (tokenGranter == null) {
tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
// 第一次执行时为null,调用getDefaultTokenGranters()方法,获得5个父类集合,第二次就有了值
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters());
}
// CompositeTokenGranter grant()方法相当于遍历执行tokenGranter.grant()
return delegate.grant(grantType, tokenRequest);
}
};
}
return tokenGranter;
}
}
其中delegate包含了多种tokenGranter的父类,因为grant_type=password,所以我们执行的类是ResourceOwnerPasswordTokenGranter,然后里面执行了authenticationManager.authenticate()和userDetailsService.loadUserByUsername();
5.CreateOauthToken,存入数据库
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// Only create a new refresh token if there wasn't an existing one
// associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in
// the case that the old access token
// expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
// 存入数据库中表AccessToken
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
// 存入数据库中表RefreshToken
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
}