SpringSecurity框架简介:SpringSecurity框架基于Spirng框架,为企业级的用户认证,用户授权,安全防护等提供一系列成熟的解决方案。用户的认证是为了让系统知道使用系统的用户是谁,而用户授权是限制用户在系统中都能干些什么,这里只讨论用户的认证这个模块。
SpringSecurity认证原理:往Web应用中注入一组过滤器链,每个过滤器都拦截一个特定的请求或者处理特定的任务。下面结合用户名密码表单登录介绍SpringSecurtiy框架中的几个重要的对象或者角色,
几个重要的类,或者角色:
1.Authentication:用户信息存储在SpringSecurity中的最终形态,这是一个接口,不同的登录方式需要构造不同的Authentication的实现。表单登录时就需要构造一个UsernamePasswordAuthenticationToken对象。
2.AuthenticationManager:全局唯一,他的作用是收集和管理系统中的所有的AuthenticatonProvider(Authentication对象的加工处理器)
3.AuthenticationProvider
一个AuthenticationProvider的实现只处理一种类型的Authentication对象,通过supports方法告诉AuthenticationManager他处理哪种类型的Authentication ,之后他会调用UserdetailService的实现去业务系统查找用户,校验密码等信息,全部成功后返回已授权的Authentication对象。
4.UserDetailService
SpringSecurity用来查询用户信息的接口,他只有有一个loadUserByUsername方法,需要客户端实现这个接口来查询数据源中的用户信息,最后返回一个UserDetail对象(他是SpringSecurity对用户信息的封装,可以用自己的User类去实现这个接口,也可以用他默认的UserDetail的实现)。
就拿用户名密码表单登录请求来说,他是由过滤链上的UsernamePasswordAuthenticationFilter过滤器拦截的,下面是该过滤器的构造器和验证方法。
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
// ~ Methods
// ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
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);
}
可以看出他默认拦截url为“/login“的post请求,当然也可以配置成其他的url。他会从请求中拿到username和password构造一个未授权过的UsernamePasswordAuthenticationToken对象,之所以说是未授权是因为
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
从构造函数看出来,他的Authenticated属性设为false,而且Authentication对象的detail属性为空,其实认证的过程就是尝试将Authentication对象的authentiated的属性设置为true并设置detail信息和权限信息的过程,过滤器中setdetails方法是将请求,session等信息组装到Authentication对象中,构造完毕后会交由AuthenticationManger(所有处理类的领导)去寻找处理该类型的Authencation对象的AuthenticationProvider(具体的处理类)去执行认证方法。下面是部分AuthenticationManger的实现类ProviderService中的代码:
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;
}
}
可以看出他会遍历他所管理的AuthenticaitonProvider,通过AuthenticationProvider的提供的supports方法寻找合适的AuthenticationProvider去加工处理该Authentication对象,在此登录方式中处理该UsernamePasswordAuthenticationToken对象的AuthenticationProvider是DaoAuthenticationProvider,他的大部分实现逻辑都写在了他的抽象父类里
public boolean supports(Class> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
父类里的supports方法告诉了AuthenticationManager他只处理UsernamePasswordAuthenticationToken这种Authentication类型。认证的代码如下:
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;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
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 {
preAuthenticationChecks.check(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;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
大致的思路就是先从未授权的Authentication对象中拿到用户名信息,密码信息,执行retrieveUser方法:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
即调用UserDetailService的实现类(在业务系统中需要自行实现这个接口)去数据源查找用户,并通过PasswordEncoder进行密码校验,若校验通过,拼装detail信息,继续执行父类的校验方法,进行用户的检查,最后return createSuccessAuthentication(principalToReturn, authentication, user);即返回一个经过授权的Authentication对象。
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(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
最后将经过授权的Authentication对象简单包装到SecurityContext中(其实就是重写了hashcode和equesls方法),最后将SecurityContext放在SecurityContextHolder中的TheadLocal变量中,用于在线程内共享用户信息,最后经过一个SecurityContextPersistenceFilter过滤器,他在处在整个过滤器链的最前端,他有两个作用,一个是请求进来时,检查session中有无SecurityContext,若有放在线程里,请求出去的时候,检查线程里有无SecurityContext,有的话放在session里,这样,就完成了整个登录流程。
SpringSocial框架简介:旨在将系统连接社交网络,如QQ、微信、微博等。基于Oauth2协议,对主流的社交网络都有很好的支持。下面是对SpringSocial及其原理具体的介绍。
SpringSocial是基于SpringSecurity框架的,他其实就是往SpringSecurity的过滤器链中添加了自己的过滤器,并使其生效,所以,SpringSecurity的基本原理在SpringSocial中也适用,只不过他SpringSocial中有一些特定的实现,下面是SpringSocial中必须要了解几个重要的角色。
1.Connection :为统一管理各式各样的第三方用户信息,SpringSocial使用Connection接口用来存储用户第三方用户信息的标准的数据结构,他是由ConnectionFacotory的工厂方法创建的
2.ConnectionFacotory:提供工厂方法用来创建Connection对象,需提供两个核心组件
1)ServiceProvider 用来获取第三方用户信息 ,每种登录方式都要提供该登录方式特有的ServiceProvider的实现,需要提供两个组件
(一)、Oauth2Template 用来走整个Oauth流程获取accessToken,SpringSocial封装accessToken的对象为AccessGrant ,他封装了Oauth2协议请求令牌时的标准的返回如令牌,刷新令牌,超时时间等。对Oauth2协议不太了解的可以先去了解Oauth2的基本概念,这里不做赘述。
(二)、 Api :拿Oauth2Template获得的accessToken,和一些必备参数,如openId(用户在服务提供商的id,在QQ登录中需要通过accessToken去获取,而微信在返回accessToken时就返回用户在微信的openId了)等去第三方应用获取用户信息。需要根据不同的登录方式提供自己的Api并实现获取用户信息的接口。
2) ApiAdater :用来将Api接口获得的用户信息适配成标准的第三方信息Connection对象
3.UsersConnectionRepository 用来对Connection对象做一些增删改查的操作。SpringSocial提供的表结构如下
-- This SQL contains a "create table" that can be used to create a table that JdbcUsersConnectionRepository can persist
-- connection in. It is, however, not to be assumed to be production-ready, all-purpose SQL. It is merely representative
-- of the kind of table that JdbcUsersConnectionRepository works with. The table and column names, as well as the general
-- column types, are what is important. Specific column types and sizes that work may vary across database vendors and
-- the required sizes may vary across API providers.
create table UserConnection (userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
rank int not null,
displayName varchar(255),
profileUrl varchar(512),
imageUrl varchar(512),
accessToken varchar(512) not null,
secret varchar(512),
refreshToken varchar(512),
expireTime bigint,
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
他是通过userId(用户在也业务系统的id),providerId(服务提供商的id),providerUserId(即OpenId)做主键标识某个业务系统的用户已经与某个服务提供商的某个用户做了绑定了。
下面介绍具体流程:SpringSocial拦截第三方登录请的的过滤器为SocialAuthenticationFilter,他默认拦截以/auth/+ providerId(服务提供商的id,客户端自己定义,如qq)。
下面是过滤器的核心处理方法:
private Authentication attemptAuthService(final SocialAuthenticationService> authService, final HttpServletRequest request, HttpServletResponse response)
throws SocialAuthenticationRedirectException, AuthenticationException {
final SocialAuthenticationToken token = authService.getAuthToken(request, response);
if (token == null) return null;
Assert.notNull(token.getConnection());
Authentication auth = getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
return doAuthentication(authService, request, token);
} else {
addConnection(authService, request, token, auth);
return null;
}
}
可以看出他首先要去通过SocialAuthenticationService的实现类去构造一个的未授权SocialAuthenticationToken对象,在这里SocialAuthenticationService的具体实现类为Oauth2AuthenticationService,具体的实现如下:
public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
String code = request.getParameter("code");
if (!StringUtils.hasText(code)) {
OAuth2Parameters params = new OAuth2Parameters();
params.setRedirectUri(buildReturnToUrl(request));
setScope(request, params);
params.add("state", generateState(connectionFactory, request));
addCustomParameters(params);
throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
} else if (StringUtils.hasText(code)) {
try {
String returnToUrl = buildReturnToUrl(request);
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
// TODO avoid API call if possible (auth using token would be fine)
Connection connection = getConnectionFactory().createConnection(accessGrant);
return new SocialAuthenticationToken(connection, null);
} catch (RestClientException e) {
logger.debug("failed to exchange for access", e);
return null;
}
} else {
return null;
}
}
他主要干了两件事:1.如果请求种没有code(授权码,Oauth2协议种授权码模式第一步的返回,用来保存用户勾选的权限等信息,换取accessToken),说明用户没有登陆,即拼装必备的参数跳向提三方应用去让用户登录
2. 如果请求中有code,就说明用户是在第三方登陆了跳转回来 ,就拿code换取AccessGrant并通过上文提到的ConectionFacotory去获取Connection,并将Connection(以三方用户信息)封装成一个未授权的SocialAuthenticationToken返回给过滤器,如果SecurityContext中没有Authentication或者该SocialAuthenticaiton没有被经过认证,就执行:
if (auth == null || !auth.isAuthenticated()) {
return doAuthentication(authService, request, token);
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type"); Assert.isTrue(!authentication.isAuthenticated(), "already authenticated"); SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication; String providerId = authToken.getProviderId(); Connection> connection = authToken.getConnection(); String userId = toUserId(connection); if (userId == null) { throw new BadCredentialsException("Unknown access token"); } UserDetails userDetails = userDetailsService.loadUserByUserId(userId); if (userDetails == null) { throw new UsernameNotFoundException("Unknown connected account id"); } return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails)); }
该方法通过AuthenticationManager寻找处理SocialAuthenticationToken的AuthenticationProvider,这是SocialAuthenticationProvider,他会从未授权的SocialAuthenticationToken中拿到providerId和providerUserId,然后使用JdbcUserConnectionReopsity去UserConnection表中查找用户在业务系统中的userId,如果查到了,则调用SocailUserDetailService(就是第三方登陆中的UserDetailService)的实现类去查找业务系统的用户信息,验证成功后拼装成一个已经授权过的SocialAuthenticationToken对象,放到SecurityContext中,完成第三方的登录。
以上就是SpringSocial的基本原理。