一:简介
简单说,OAuth就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取部分允许获取的数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
- 令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
- 令牌可以被数据所有者撤销,会立即失效。密码一般不允许被他人撤销。
- 令牌有权限范围(scope),密码一般是完整权限。
相关文章
二:代码
1. pom.xml
注意:这里使用的springboot的版本为2.1.5.RELEASE,不同的版本功能实现上可能会有差异。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-security-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-security-example</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. application.yml
注意:在学习Spring的新技术时最好打开debug日志,这样控制台会输出更多有用的信息,可以根据控制台的输出日志来了解该功能使用到的主要的类,涉及到的主要方法等。
application.yml的配置是可选的。因为服务端口默认是8080,日志的配置也不是必须的。
server:
port: 8080
logging:
level:
org.springframework: debug
3. UserDetailsService
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
4. SecurityConfiguration
注意:关于密码加密器PasswordEncoder,如果使用加密可以使用BCryptPasswordEncoder,如果不加密可以自己实现一个PasswordEncoder的实现类
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().csrf().disable();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// @Bean
// public PasswordEncoder passwordEncoder() {
// 对原始字符不进行加密,比较时都会返回true
// return new PasswordEncoder() {
// @Override
// public String encode (CharSequence charSequence) {
// return charSequence.toString();
// }
// @Override
// public boolean matches(CharSequence charSequence, String s) {
// return true;
// }
// };
// }
}
5. 认证服务器 AuthorizationServer
注意:对于clients.secret设置,网上大部分都是直接赋值一个明文,究竟设置为明文还是密文取决与SecurityConfiguration类中配置的PasswordEncoder是什么,如果PasswordEncoder为BCryptPasswordEncoder,此时clients.secret也必须设置为BCryptPasswordEncoder加密后的密文,如果PasswordEncoder为上文注释的自定义的密码加密器(该实现任何情况下都会返回true),此时clients.secret可以设置为明文。
@Configuration
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 使用in-memory存储
.withClient("clientId")
.secret(new BCryptPasswordEncoder().encode("clientSecret"))
.authorizedGrantTypes("authorization_code")
.scopes("all")
.redirectUris("http://www.baidu.com");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
三:获取access_token
-
在浏览器访问 localhost:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=http://www.baidu.com 会跳转到http://localhost:8080/login,输入用户名这里输入的是admin(其实用户名可以输入任何字符),密码123456(密码必须和UserDetailsService配置的用户密码保持一致)
-
登录成功后跳转到授权页面,选择同意Approve并点击授权Authorize
-
授权成功后会跳转到AuthorizationServer配置的redirectUris上,这里配置的是百度,会跳转到www.baidu.com并携带一个code参数
-
根据上个步骤获取到的code值,然后获取access_token
4.1 使用POST请求方式访问 http://localhost:8080/oauth/token
4.2 设置请求头Authorization,Username设置为我们设置的clientId,Password为我们设置的clientSecret
4.3 参数 grant_type的值固定为authorization_code,code 为上个步骤获取到的值,其它值都是认证服务器配置的值
这里使用了一个Chrome的发请求的客户端插件 Restlet Client - REST API Testing ,如果没有可以使用Postman来代替
四:/oauth/token源码分析
- TokenEndpoint: token结束点,可以理解为Token Controller,用于接收"/oauth/token"请求
- InMemoryClientDetailsService implements ClientDetailsService: 根据clientId读取第三方应用的配置信息(ClientDetails)
- TokenRequest: 封装"/oauth/token"中请求的参数和ClientDetails
- CompositeTokenGranter implements TokenGranter
- OAuth2Request: 组装 ClientDetails 和 TokenRequest对象
- Authentication: 授权用户信息, 从UserDeatails中获取的
- OAuth2Authentication: 组装OAuth2Request和Authentication对象
- DefaultTokenServices implements AuthorizationServerTokenServices: 生成令牌 OAuth2AccessToken
- InMemoryTokenStore implements TokenStore: 令牌的存取和删除(TokenStore的实现类有InMemoryTokenStore、JdbcTokenStore、JwtTokenStore、JwkTokenStore、RedisTokenStore)
- TokenEnhancer: 用于改造令牌(JwtAccessTokenConverter)
@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.");
}
// 获取clientId
String clientId = getClientId(principal);
// 获取第三方应用的配置信息
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
// 组装parameters和authenticatedClient
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
if (!clientId.equals(tokenRequest.getClientId())) {
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)) {
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
// 生成访问令牌
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
}
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) {
// authorization_code -> AuthorizationCodeTokenGranter
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
}
public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = tokenRequest.getRequestParameters();
String authorizationCode = parameters.get("code");
String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);
if (authorizationCode == null) {
throw new InvalidRequestException("An authorization code must be supplied.");
}
OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
if (storedAuth == null) {
throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
}
OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
OAuth2Utils.REDIRECT_URI);
if ((redirectUri != null || redirectUriApprovalParameter != null)
&& !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
throw new RedirectMismatchException("Redirect URI mismatch.");
}
String pendingClientId = pendingOAuth2Request.getClientId();
String clientId = tokenRequest.getClientId();
if (clientId != null && !clientId.equals(pendingClientId)) {
// just a sanity check.
throw new InvalidClientException("Client ID mismatch");
}
Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request.getRequestParameters());
combinedParameters.putAll(parameters);
OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
Authentication userAuth = storedAuth.getUserAuthentication();
return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);
}
}
public abstract class AbstractTokenGranter implements TokenGranter {
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);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
}
/**
* 将 ClientDetails 和 TokenRequest组装成OAuth2AccessToken
*
*/
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
}
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();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
if (refreshToken == null) {
// 创建refreshToken
refreshToken = createRefreshToken(authentication);
}
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 创建access_token
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
}