慕课笔记-Spring Security Oauth开发APP认证框架1
文章目录
1. 为什么需要开发App社交登陆?
如果用户通过一些应用如app来登陆,那么就没有session,
如果一定要用session开发,也是可以的,但是
- 开发繁琐。
- 安全性和客户体验差
- 有些前端技术不支持cookie,如小程序
因此当我们的访问者不是浏览器的时候,用另外一种方式来访问
用发令牌的方式来访问资源
2. Spring Security OAuth介绍
绿色是实现好的,要实现的是自定义的认证
在资源上添加filter,实现对资源的保护
3. 实现一个标准的OAuth2协议的Provider角色
前面的demo项目都是依赖browser项目,这里依赖app项目,修改pom文件
<dependency>
<groupId>com.imooc.security</groupId>
<artifactId>imooc-security-app</artifactId>
<version>${imooc.security.version}</version>
</dependency>
此时app项目是个空项目,直接启动。
- 报错1:提示没有成功失败处理器,copy browser中的处理器到app下面。
- 报错2:提示没有密码加密BEAN,把这个bean的配置放到公共的core项目中
之后再次启动,就没有报错了。
3.1 实现认证服务器:
新建一个类,只需要加上下面注解,就实现了一个认证服务器了,由于demo项目依赖于app项目
Configuration
@EnableAuthorizationServer
public class ImoocAuthorizationServerConfig
因此此时demo项目就是一个认证服务器了,就能提供OAuth的认证服务
- 授权码模式:
查看oauth协议->4.1,授权码模式需要的参数:- Code:必须为code
- Clientid:哪个应用需要授权,服务商决定哪些第三方需要授权
在浏览器中输入:
http://localhost:8080/oauth/authorize?response_type=code&client_id=imooc&request_rui=http://example.com&scope=all
此时需要输入用户名密码
目前我们的角色是qq/weixin服务提供商角色,引导用户给第三方应用授权。
做为服务提供商我需要知道三件事:
- 哪一个应用在请求授权:即传入的client_id就知道是给百度还是mooc授权,这个clientid是服务器给分配的
- 需要服务提供商系统中的哪个用户给你授权:即这里输入的用户名密码对应的用户
- 给什么类型的权限:通过scope的参数,这里的参数是服务提供商自定义的
这里有用户名与密码,最终会调用到userdetailservice来处理
@Component
@Transactional
public class DemoUserDetailsService implements UserDetailsService, SocialUserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private PasswordEncoder passwordEncoder;
/*
* (non-Javadoc)
*
* @see org.springframework.security.core.userdetails.UserDetailsService#
* loadUserByUsername(java.lang.String)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// logger.info("表单登录用户名:" + username);
// Admin admin = adminRepository.findByUsername(username);
// admin.getUrls();
// return admin;
return buildUser(username);
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
logger.info("设计登录用户Id:" + userId);
return buildUser(userId);
}
private SocialUserDetails buildUser(String userId) {
// 根据用户名查找用户信息
//根据查找到的用户信息判断用户是否被冻结
String password = passwordEncoder.encode("123456");
logger.info("数据库密码是:"+password);
return new SocialUser(userId, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
这里的密码默认是123456.
登录后到403页面。
这是因为默认情况下,用户必须具有ROLE_USER的角色
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER"));
添加之后重新登陆
目前的client_id是每次服务提供商自动生成的,这里可以配置为一个固定的。
这个页面相当于qq/weixin扫码的页面。
此时相当于回到第三方应用的页面。
拿 着这个code来获取token
- 密码模式
这里的用户名密码就是用户在服务提供商(qq/weixin)的用户名密码
在互联网中这种模式是不可用的,但是在公司内部这种模式是可用的。
注意这个token与上面的授权码模式的token是一样的。
只要这个token没过期,针对同一个用户的token是一致的。
到目前为止,我们已经实现了一个认证服务器,其实核心代码就一行:
3.2. 实现资源服务器
其实也只是一个注解的事
@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig
测试:访问资源
如果什么都不加,会报401未授权错误
重启服务,重新拿到token为什么需要重启?因为默认的token是存储在内存中,如果服务关掉,上面生成的token就被清掉了。使用下面工具来测试
需要加上headers: Authoriztion
内容是bearer token
响应:
4.源码分析(密码模式)
TokenEndpoint:可以理解为一个control
ClientDetailsService:读取第三方应用的配置令牌到ClientDetails中去
TokenRequest:封装了ClientDetails与其它的令牌,如授权模式,授权code等
TokenGranter:里面封装的是中种授权模式的实现,会根据传入的授权模式选择一种实现,来执行令牌生成的逻辑
无论是哪种实现,都生成过程中都会产生两个对象
OAuth2Request其实是前面TokenRequest与ClientDetails的整合
Authenticaiotn 封装的授权用户的信息,如当前是谁在对第三方进行授权
上面这两个对象组合为OAuth2Authentication:这个对象包含哪个第三方应用,在请求谁进行授权,授权模式等
这个对象会传给AutorizationSeriveTokenServices,它拿到上面的令牌,最终生成一个令牌
TokenStore:令牌存储
TokenEnhancer:令牌增强器
TokenEndpoint#postAccessToken
getClientId->CreateTokenRequest->检查scope(什么样的授权)->granttype(哪种授权模式)->授权码模式处理->刷新令牌处理->传给tokenGranter(里面封装了四种模式,再加上refresh token)->选择一个进行授权->tokenservice重构之前的三种认证方式的代码,使其支持Token
@RequestMapping(value = "/oauth/token", method=RequestMethod.GET) //授权的url
public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!allowedRequestMethods.contains(HttpMethod.GET)) {
throw new HttpRequestMethodNotSupportedException("GET");
}
return postAccessToken(principal, parameters);
}
@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);//获得第三方应用的id
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);//获得第三方应用的Details
//利用第三方应用的令牌来创建tokenRequest
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");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {//简化模式,在授权码械下是不支持的
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {//授权的范围是最终服务器给的,不是用户请求的,因此需要先清空,后面会根据服务提供商返回的授权码重新赋权限
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {//刷新令牌请求
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
//最后得到tokenGranter,通过Granter根据授权类型来获得
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
上面的关键是最后一步,获得token,首先CompositeTokenGranter#grant,根据类型获得TokenGranter
public class CompositeTokenGranter implements TokenGranter {
private final List<TokenGranter> tokenGranters;
public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
调用TokenGranter.grant方法,通过树图得知总共有5种方式,此外我们使用的是ResourceOwnerPasswordTokenGranter.grant方法
由于这个类并没有这个方法,因此调用其父类的,也就是AbstractTokenGranter#grant
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
logger.debug("Getting access token for: " + clientId);
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
这里的getOAuth2Authentication(client, tokenRequest),则还是调用前面的ResourceOwnerPasswordTokenGranter的实现。
这里牵涉到java的基础,子类与父类方法具体调用哪一种的问题。
在ResourceOwnerPasswordTokenGranter#getOAuth2Authentication
实现里我们可以看到细节
最终的OAuth2Authentication对象是由OAuth2Request与授权用户令牌即Authentication两部分生成的,可以理解为最后token的生成是由请求与授权用户两部分构成的。
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String password = parameters.get("password");
// Protect from downstream leaks of password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
//由authenticationManager来认证用户令牌,这里面会走userdetailsrvice
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
查看下DefaultTokenServices#createAccessToken
这个代码,来看token是怎么生成的
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);//首先去tokenstore里面去拿
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);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
5. 自定义登陆
上面源码中的是获取信息的请求处理的流程,我们需要做的是用户登陆后获得令牌,然后来访问其它资源,因此在tokengranter之前的都是不可用的,但是后面我们可以构建符合要求的参数利用令牌服务生成令牌这部分。
重写登陆(对于请求头的处理可参照BasicAuthenticationFilter#doFilterInternal
)
ImoocAuthenticationSuccessHandler#onAuthenticationSuccess
安全配置类
@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig extends ResourceServerConfigurerAdapter {
用工具模拟表单提交
注意这里要带头的处理
拿着token来获得获得用户信息
5.1自定义验证码登陆
以前的验证码会把验证码放入到session中,但是针对app,验证码是没法存的
使用这个工具发的话,也是基于浏览器的,是带cookie的返回的是200,是正常的。转化为linux的curl命令
原理逻辑:
思路就是把验证码存放到外部AbstractValidateCodeProcessor
发送之后获得验证码
再次添加上验证码来请求资源
5.2 自定义社交登陆
-
- 简化模式
服务提供商返回的是openid和accesstoken
OpenIdAuthenticationFilter
- 简化模式
这个类会把令牌传给给providermanger,然后找到provider来验证
因此还需要写一个验证的类
OpenIdAuthenticationProvider
最后写一个配置类
OpenIdAuthenticationSecurityConfig
上面拿到token
拿到token获取用户信息
-
- 验证码模式
需要模拟第4步
思路:在浏览器模式下打一个断点,获得授权码
拿这个授权码使用app
- 验证码模式
- . 重构注册流程
上面成功是建立在有用户的情况下
如果是用户第一次进入,需要注册。
在app环境下,以前的注册流程是走不通的,因为没有seesion
Spring提供的ProviderSignInUtils是基于session的,此处不能用。
需要自定义一个基于redis的工具
AppSingUpUtils