Oauth2的学习总结
这段时间领导叫我学习oauth2准备一下,以后上spring cloud的时候可以上手就使用。陆陆续续学习了两周,三周还是更多,发现oauth2还是很复杂的。这篇文章就是想总结一下这段时间学习oauth2的东西,希望可以给刚学习oauth2的做一些铺垫,有些地方不对的还希望指正。
1.oauth2的简单介绍
oauth2总体我的理解是做认证授权的,最常见的就是第三方认证(QQ,微信,淘宝等),还有就是微服务的时候做认证使用。具体的概念,可以详见oauth2的解释,详细的oauth2的原理可以看大佬的博客
2.oauth2的四种模式
1.简单模式(implicit)
一般不使用,完全不可信
2.客户端模式(client credentials)
客户端是可信的,只要传入一些参数就能获取access token
3.密码模式(authorizaiton code)
需要提供用户名和密码去实现获取access token
4.code模式(password)
完整的模式,先获取code,然后通过code去获取access token
3.具体实现
3.1 本文实现的逻辑
3.2 代码逻辑的实现
3.2.1 认证中心实现
AuthorizationServerConfigurerAdapter这个是用来设定认证中心相关配置
@Configuration
@EnableAuthorizationServer
public class OAuth2Configure extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Bean
public MyRedisTokenStore tokenStore(){
return new MyRedisTokenStore(redisConnectionFactory);
}
//client模式的设定
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
clients.inMemory()
.withClient("user-service")
.secret("{noop}123456") //{noop}代表的是明文,这个和加密模式相关
.scopes("service")
.authorizedGrantTypes("refresh_token","password","client_credentials")
.accessTokenValiditySeconds(60)
.refreshTokenValiditySeconds(6000)
.and()
.withClient("client-service")
.secret("{noop}123456")
.scopes("client")
.authorizedGrantTypes("refresh_token","client_credentials")
.accessTokenValiditySeconds(60)
.refreshTokenValiditySeconds(6000);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
//设定userDetailsService,若无,refresh_token会有UserDetailsService is required错误
endpoints.authenticationManager(authenticationManager).userDetailsService(myUserDetailsService).tokenStore(tokenStore());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()");
// .checkTokenAccess("isAuthenticated()");
}
}
重写redis,如果调用原先的有的RedisTokenStore的话会报错,会报get方法没有啥的(之前查到以后忘记掉错误是啥了)
@Component
public class MyRedisTokenStore implements TokenStore {
private static final String ACCESS = "access:";
private static final String AUTH_TO_ACCESS = "auth_to_access:";
private static final String AUTH = "auth:";
private static final String REFRESH_AUTH = "refresh_auth:";
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
private static final String REFRESH = "refresh:";
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
private static final String UNAME_TO_ACCESS = "uname_to_access:";
private final RedisConnectionFactory connectionFactory;
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
private String prefix = "";
public MyRedisTokenStore(RedisConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}
public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
this.serializationStrategy = serializationStrategy;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
private RedisConnection getConnection() {
return this.connectionFactory.getConnection();
}
private byte[] serialize(Object object) {
return this.serializationStrategy.serialize(object);
}
private byte[] serializeKey(String object) {
return this.serialize(this.prefix + object);
}
private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
return (OAuth2AccessToken) this.serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
}
private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
return (OAuth2Authentication) this.serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
}
private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
return (OAuth2RefreshToken) this.serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
}
private byte[] serialize(String string) {
return this.serializationStrategy.serialize(string);
}
private String deserializeString(byte[] bytes) {
return this.serializationStrategy.deserializeString(bytes);
}
@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = this.authenticationKeyGenerator.extractKey(authentication);
byte[] serializedKey = this.serializeKey(AUTH_TO_ACCESS + key);
byte[] bytes = null;
RedisConnection conn = this.getConnection();
try {
bytes = conn.get(serializedKey);
} finally {
conn.close();
}
OAuth2AccessToken accessToken = this.deserializeAccessToken(bytes);
if (accessToken != null) {
OAuth2Authentication storedAuthentication = this.readAuthentication(accessToken.getValue());
if (storedAuthentication == null || !key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))) {
this.storeAccessToken(accessToken, authentication);
}
}
return accessToken;
}
@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return this.readAuthentication(token.getValue());
}
@Override
public OAuth2Authentication readAuthentication(String token) {
byte[] bytes = null;
RedisConnection conn = this.getConnection();
try {
bytes = conn.get(this.serializeKey("auth:" + token));
} finally {
conn.close();
}
OAuth2Authentication auth = this.deserializeAuthentication(bytes);
return auth;
}
@Override
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return this.readAuthenticationForRefreshToken(token.getValue());
}
public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
RedisConnection conn = getConnection();
try {
byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token));
OAuth2Authentication auth = deserializeAuthentication(bytes);
return auth;
} finally {
conn.close();
}
}
@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
byte[] serializedAccessToken = serialize(token);
byte[] serializedAuth = serialize(authentication);
byte[] accessKey = serializeKey(ACCESS + token.getValue());
byte[] authKey = serializeKey(AUTH + token.getValue());
byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
RedisConnection conn = getConnection();
try {
conn.openPipeline();
conn.stringCommands().set(accessKey, serializedAccessToken);
conn.stringCommands().set(authKey, serializedAuth);
conn.stringCommands().set(authToAccessKey, serializedAccessToken);
if (!authentication.isClientOnly()) {
conn.rPush(approvalKey, serializedAccessToken);
}
conn.rPush(clientId, serializedAccessToken);
if (token.getExpiration() != null) {
int seconds = token.getExpiresIn();
conn.expire(accessKey, seconds);
conn.expire(authKey, seconds);
conn.expire(authToAccessKey, seconds);
conn.expire(clientId, seconds);
conn.expire(approvalKey, seconds);
}
OAuth2RefreshToken refreshToken = token.getRefreshToken();
if (refreshToken != null && refreshToken.getValue() != null) {
byte[] refresh = serialize(token.getRefreshToken().getValue());
byte[] auth = serialize(token.getValue());
byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
conn.stringCommands().set(refreshToAccessKey, auth);
byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
conn.stringCommands().set(accessToRefreshKey, refresh);
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
Date expiration = expiringRefreshToken.getExpiration();
if (expiration != null) {
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
.intValue();
conn.expire(refreshToAccessKey, seconds);
conn.expire(accessToRefreshKey, seconds);
}
}
}
conn.closePipeline();
} finally {
conn.close();
}
}
private static String getApprovalKey(OAuth2Authentication authentication) {
String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication().getName();
return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
}
private static String getApprovalKey(String clientId, String userName) {
return clientId + (userName == null ? "" : ":" + userName);
}
@Override
public void removeAccessToken(OAuth2AccessToken accessToken) {
this.removeAccessToken(accessToken.getValue());
}
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
byte[] key = serializeKey(ACCESS + tokenValue);
byte[] bytes = null;
RedisConnection conn = getConnection();
try {
bytes = conn.get(key);
} finally {
conn.close();
}
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
return accessToken;
}
public void removeAccessToken(String tokenValue) {
byte[] accessKey = serializeKey(ACCESS + tokenValue);
byte[] authKey = serializeKey(AUTH + tokenValue);
byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
RedisConnection conn = getConnection();
try {
conn.openPipeline();
conn.get(accessKey);
conn.get(authKey);
conn.del(accessKey);
conn.del(accessToRefreshKey);
// Don't remove the refresh token - it's up to the caller to do that
conn.del(authKey);
List<Object> results = conn.closePipeline();
byte[] access = (byte[]) results.get(0);
byte[] auth = (byte[]) results.get(1);
OAuth2Authentication authentication = deserializeAuthentication(auth);
if (authentication != null) {
String key = authenticationKeyGenerator.extractKey(authentication);
byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
conn.openPipeline();
conn.del(authToAccessKey);
conn.lRem(unameKey, 1, access);
conn.lRem(clientId, 1, access);
conn.del(serialize(ACCESS + key));
conn.closePipeline();
}
} finally {
conn.close();
}
}
@Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue());
byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue());
byte[] serializedRefreshToken = serialize(refreshToken);
RedisConnection conn = getConnection();
try {
conn.openPipeline();
conn.stringCommands().set(refreshKey, serializedRefreshToken);
conn.stringCommands().set(refreshAuthKey, serialize(authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
Date expiration = expiringRefreshToken.getExpiration();
if (expiration != null) {
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
.intValue();
conn.expire(refreshKey, seconds);
conn.expire(refreshAuthKey, seconds);
}
}
conn.closePipeline();
} finally {
conn.close();
}
}
@Override
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
byte[] key = serializeKey(REFRESH + tokenValue);
byte[] bytes = null;
RedisConnection conn = getConnection();
try {
bytes = conn.get(key);
} finally {
conn.close();
}
OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes);
return refreshToken;
}
@Override
public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
this.removeRefreshToken(refreshToken.getValue());
}
public void removeRefreshToken(String tokenValue) {
byte[] refreshKey = serializeKey(REFRESH + tokenValue);
byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue);
byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue);
byte[] access2RefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
RedisConnection conn = getConnection();
try {
conn.openPipeline();
conn.del(refreshKey);
conn.del(refreshAuthKey);
conn.del(refresh2AccessKey);
conn.del(access2RefreshKey);
conn.closePipeline();
} finally {
conn.close();
}
}
@Override
public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
this.removeAccessTokenUsingRefreshToken(refreshToken.getValue());
}
private void removeAccessTokenUsingRefreshToken(String refreshToken) {
byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken);
List<Object> results = null;
RedisConnection conn = getConnection();
try {
conn.openPipeline();
conn.get(key);
conn.del(key);
results = conn.closePipeline();
} finally {
conn.close();
}
if (results == null) {
return;
}
byte[] bytes = (byte[]) results.get(0);
String accessToken = deserializeString(bytes);
if (accessToken != null) {
removeAccessToken(accessToken);
}
}
public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
List<byte[]> byteList = null;
RedisConnection conn = getConnection();
try {
byteList = conn.lRange(approvalKey, 0, -1);
} finally {
conn.close();
}
if (byteList == null || byteList.size() == 0) {
return Collections.<OAuth2AccessToken>emptySet();
}
List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
for (byte[] bytes : byteList) {
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
accessTokens.add(accessToken);
}
return Collections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);
}
@Override
public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
List<byte[]> byteList = null;
RedisConnection conn = getConnection();
try {
byteList = conn.lRange(key, 0, -1);
} finally {
conn.close();
}
if (byteList == null || byteList.size() == 0) {
return Collections.<OAuth2AccessToken>emptySet();
}
List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
for (byte[] bytes : byteList) {
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
accessTokens.add(accessToken);
}
return Collections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);
}
}
设定获取user的接口,用来作为认证接口(user权限的接口)
@RestController
@RequestMapping("/users")
public class UserController {
Logger logger = LoggerFactory.getLogger(UserController.class);
@RequestMapping(value = "/current", method = RequestMethod.GET)
public Principal getUser(Principal principal) {
logger.info(">>>>>>>>>>>>>>>>>>>>>>>>");
logger.info(principal.toString());
logger.info(">>>>>>>>>>>>>>>>>>>>>>>>");
return principal;
}
}
为了打开user的端口,还需要资源配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.authorizeRequests()
.antMatchers("/actuator/**", "/oauth/**","/token/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
3.2.2 client端实现
资源接口配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// .requestMatchers()
// .antMatchers("/hello","/hello/**")
// .and()
.authorizeRequests()
.antMatchers("/hello","/hello/**").authenticated().accessDecisionManager(myAccessDecisionManager)
/**
antMatchers("/hello","/hello/**").authenticated().accessDecisionManager(myAccessDecisionManager)
是用来确认是不是能用AccessDecisionMangager是不是能搞定权限(答案是可以的),不需要的可以用上面的方法就好了
*/
// .and()
// .authorizeRequests()
// .antMatchers("/hello","/hello/**").authenticated()
// .and()
// .authorizeRequests()
// .antMatchers("/hello","/hello/**").hasAuthority(null)
.and()
.authorizeRequests()
.antMatchers("/actuator/**", "/oauth/**","/token/**").permitAll()
.and()
.formLogin().permitAll();
}
}
MyDecideAccessDecisionManager的实现,主要是decide方法;完全是为了测试,所以里面的方法都是自己瞎定义的,完全为了方便
@Slf4j
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributeCollection)
throws AccessDeniedException, InsufficientAuthenticationException{
HttpServletRequest httpRequest = ((FilterInvocation)object).getHttpRequest();
StringBuffer stringBuffer=new StringBuffer();
authentication.getAuthorities().stream().forEach(s->stringBuffer.append(s));
log.info("Authentication information:"+stringBuffer);
log.info("url information:"+httpRequest.getRequestURI());
StringBuffer stringBufferConfigAttributeCollection=new StringBuffer();
authentication.getAuthorities().stream().forEach(s->stringBufferConfigAttributeCollection.append(s));
log.info("configAttributeCollection information:"+stringBufferConfigAttributeCollection);
if(httpRequest.getRequestURI().equals("/hello/test")){
return;
}
/**
* 测试证明PreAuthorize还是会参与权限判断,但是可以通过decide去做了
* 不需要使用PreAuthorize再去做一次
*/
if(httpRequest.getRequestURI().equals("/hello")){
return;
}
throw new AccessDeniedException("AccessDecisionManager Access Denied");
}
public boolean supports(ConfigAttribute var1){
return true;
}
public boolean supports(Class<?> var1){
return true;
}
}
测试使用的controller
@RestController
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PreAuthorize(value=("hasAuthority('Role_Admin')"))
@GetMapping("/oauth2")
public String oauth(){
return "hello,oauth2";
}
@PreAuthorize(value=("hasAuthority('Role_Admin')"))
@GetMapping("/hello/test")
public String test(){
return "hello,test";
}
}
client端的配置文件
server:
port: 8091
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/tms?useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&useSSL=false
username: root
password:
application:
name: test-service
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.dfcj.oauth2security.dao
#logging:
# level:
# root: debug
###用来认证客户端使用的,很重要
security:
oauth2:
resource:
user-info-uri: http://localhost:8090/users/current
id: test-service
prefer-token-info: false
client:
scope: service
client-secret: 123456
client-id: user-service
id: user-service
4.总结
写完通篇,我发现我自己只是贴了一些自认为的重要代码,解释啥的也没说啥(其实我也不知道应该说啥,只是知道这个不简单)
自己一开始从最简单的client模式实现,然后转到password模式,然后又用password模式实现授权,然后又转到实现AccessDecisionManager去实现鉴权,辗转反复,一直在测试一直在debug,终于弄出来一点小的成果,和大家分享一下,可能看不懂,但是希望对后面做oauth2的人有帮助吧。
有问题欢迎提问,一起解决java问题