如果用户通过一些应用如app来登陆,那么就没有session,
如果一定要用session开发,也是可以的,但是
绿色是实现好的,要实现的是自定义的认证
在资源上添加filter,实现对资源的保护
前面的demo项目都是依赖browser项目,这里依赖app项目,修改pom文件
<dependency> <groupId>com.imooc.security</groupId> <artifactId>imooc-security-app</artifactId> <version>${imooc.security.version}</version> </dependency>
此时app项目是个空项目,直接启动。
新建一个类,只需要加上下面注解,就实现了一个认证服务器了,由于demo项目依赖于app项目
Configuration @EnableAuthorizationServer public class ImoocAuthorizationServerConfig
因此此时demo项目就是一个认证服务器了,就能提供OAuth的认证服务
在浏览器中输入:
http://localhost:8080/oauth/authorize?response_type=code&client_id=imooc&request_rui=http://example.com&scope=all
此时需要输入用户名密码
目前我们的角色是qq/weixin服务提供商角色,引导用户给第三方应用授权。
做为服务提供商我需要知道三件事:
@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
到目前为止,我们已经实现了一个认证服务器,其实核心代码就一行:
其实也只是一个注解的事
@Configuration @EnableResourceServer public class ImoocResourceServerConfig
测试:访问资源
如果什么都不加,会报401未授权错误
重启服务,重新拿到token为什么需要重启?因为默认的token是存储在内存中,如果服务关掉,上面生成的token就被清掉了。使用下面工具来测试
需要加上headers: Authoriztion
内容是bearer token
响应:
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; }
上面源码中的是获取信息的请求处理的流程,我们需要做的是用户登陆后获得令牌,然后来访问其它资源,因此在tokengranter之前的都是不可用的,但是后面我们可以构建符合要求的参数利用令牌服务生成令牌这部分。
重写登陆(对于请求头的处理可参照BasicAuthenticationFilter#doFilterInternal
)
ImoocAuthenticationSuccessHandler#onAuthenticationSuccess
安全配置类
@Configuration @EnableResourceServer public class ImoocResourceServerConfig extends ResourceServerConfigurerAdapter {
用工具模拟表单提交
注意这里要带头的处理
拿着token来获得获得用户信息
以前的验证码会把验证码放入到session中,但是针对app,验证码是没法存的
使用这个工具发的话,也是基于浏览器的,是带cookie的返回的是200,是正常的。转化为linux的curl命令
原理逻辑:
思路就是把验证码存放到外部AbstractValidateCodeProcessor
发送之后获得验证码
再次添加上验证码来请求资源
OpenIdAuthenticationFilter
这个类会把令牌传给给providermanger,然后找到provider来验证
因此还需要写一个验证的类
OpenIdAuthenticationProvider
最后写一个配置类
OpenIdAuthenticationSecurityConfig
上面拿到token
拿到token获取用户信息