说明:
我此次集成采用的spring boot的版本是2.0以上
Oauth采用的也是2.0
我采用的是password模式
IDE用的是idea
以下所写来自于刚开始接触oauth不到一个月的新手菜鸟,有不对的地方奇怪大神指正,有想要交流的加我qq2894908303.
问题:
1、初次接触Oauth,不知道其原理
2、不知道各个微服务之间的实现关系
3、按照网上的做法修改我自己的代码很长一段时间没有生成access_token
4、其他非认证授权微服务的鉴权很长时间没实现
5、具体方法权限的校验很长时间没实现
6、spring boot2.X和Oauth2.0的整合因为版本的原因一直启动不了项目
解决:
1、Oauth的原理是提供一个认证中心,用户登陆后会从认证中心拿到一个令牌,令牌中有用户的用户名和权限,然后用户去访问其他微服务的接口的时候,每次都带着这个令牌,其他微服务在对这个令牌进行有效性校验后就可以进行接口的访问了;实现这些操作的代码基本上都封装好了,只需要我们做一些基础配置,以及复写几个特殊的方法就好了,比如loadUserByUsername这个方法。
认证中心是重点,它可以是一个单独的微服务,也可以和你的用户微服务放到一块,这个要看你自己的设计,单独的一个微服务更正规,也更容易扩展,因为我的系统是单一业务系统,不涉及其他系统,所以我一开始是放到一块的。
2、其他非认证中心微服务都是资源微服务,在访问这些微服务之前用户必须先登录拿到令牌,和传统的相比,每次用户的访问都是无状态的,有没有登录,有没有权限进行操作,都要看你的令牌中的用户和权限。微服务的实现需要网关微服务、配置中心、ribbon等,这些自己看需要添加
3、我参照网上的方法,很长一段时间没有生成token的原因有两点,第一是版本问题,第二是密码加密那个地方。
4、导致第四个问题的原因是token的存储方法,我默认是内存,一直不知道在哪改配置,后来自己在资源微服务的配置文件中加了
@Bean
public TokenStore tokenStore() {
//使用内存的tokenStore
//return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
}
这个配置后才通的,原因下边会说。
5、我在授权的时候给用授权了,但是方法还是可以随便访问,后来才知道需要开启方法鉴权的配置,就是@EnableGlobalMethodSecurity(prePostEnabled = true)这个注解,默认是false,这个注解加在ResourceServerConfig上就行
6、这个问题是一开始就遇到的,报错根本就没提示说是版本的问题,后来百度到是版本的问题
实现认证中心步骤:
1、在你的ide中新建一个springboot项目,pom文件如下
org.springframework.boot
spring-boot-starter-parent
2.0.3.RELEASE
UTF-8
UTF-8
1.8
1.1.10
Finchley.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
此处注意oauth和springboot的版本,我一开始没注意,直接把网上springboot1点几的和oauth的集成直接拿过来,将spring boot的版本改为2.X但是oauth的没动,就一直启动不了,后来发现是版本问题
2、认证中心的几个重要配置文件
1、
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userService;
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("test")//客户端ID
.authorizedGrantTypes("password", "refresh_token")//设置验证方式
.scopes("read", "write")
.secret(new BCryptPasswordEncoder().encode("123456"))
.accessTokenValiditySeconds(10000) //token过期时间
.refreshTokenValiditySeconds(10000); //refresh过期时间
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userService); //配置userService 这样每次认证的时候会去检验用户是否锁定,有效等
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()//允许表单提交
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
/* .addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);*/
}
@Bean
public TokenStore tokenStore() {
//使用内存的tokenStore
//return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
}
}
其中tokenStore这个方法是指定你生成的token的存放地方的,有内存存储,有redis,有JDBC也就是数据库,这个地方我一开始设置的是内存存储,导致其他微服务在解析token的时候解析不了,因为两个为服务内存不共享,所以其他微服务是看不到认证中心的token存放的信息的,所以才导致第四个问题的出现,还有一点,这个配置在其他微服务中也必须写,这样才能让其他微服务知道该去哪里获取登录的token,不写的话,默认好像是内存。
还有一点要注意, .secret(new BCryptPasswordEncoder().encode(“123456”))这个配置,如果配置不好会导致授权失败,主要原因是这个地方oauth升级后变了一点,可能还有其他的实现方式,目前我就用的网上的这个方式。
其他的设置参照网上的就可以了,没有什么需要特殊注意的。
import com.sxzq.glq.user.support.GlqUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private GlqUserDetailsService glqUserDetailsService;
/**
* 认证管理
*
* @return 认证管理对象
* @throws Exception
* 认证异常信息
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* http安全配置
*
* @param http
* http安全对象
* @throws Exception
* http安全异常信息
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 允许所有用户访问"/"和"/index.html"
http.authorizeRequests()
.antMatchers("/", "/index.html","/oauth/token").permitAll()
.and()
//.anyRequest().authenticated() // 其他地址的访问均需验证权限
.formLogin()
.loginPage("/login.html") // 登录页
.failureUrl("/login-error.html").permitAll()
.and()
.csrf().disable();
/*
.and()
.logout()
.logoutSuccessUrl("/index.html")*/;
}
/**
* 全局用户信息
* 方法上的注解@Autowired的意思是,方法的参数的值是从spring容器中获取的
* 即参数AuthenticationManagerBuilder是spring中的一个Bean
*
* @param auth 认证管理
* @throws Exception 用户认证异常信息
*/
@Autowired
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(glqUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这个配置文件需要注意的地方是密码加密的设置,即passwordEncoder()这个方法,新版本的oauth和旧版本不一样
import com.sxzq.glq.user.support.GlqUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
//import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter {
@Autowired
private GlqUserDetailsService glqUserDetailsService;
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
//TODO:use md5
auth.userDetailsService(glqUserDetailsService)
.passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return charSequence.toString().equals(s);
}
});
}
}
这个配置文件目前不了解,不知道可不可以省略
import com.sxzq.glq.user.entity.UserEntity;
import com.sxzq.glq.user.service.IUserService;
import com.sxzq.glq.user.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Primary
@Service
public class GlqUserDetailsService implements UserDetailsService {
@Autowired
private IUserService userService;
/**
* 授权的时候是对角色授权,而认证的时候应该基于资源,而不是角色,因为资源是不变的,而用户的角色是会变的
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userService.getUserByUserName(username);
user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
if (null == user) {
throw new UsernameNotFoundException(username);
}
List authorities = new ArrayList<>();
/*for (String roleCode : userService.getRoleCodeList()) {
for (String permissionCode : roleService.getPermissionCodeListByRoleCode(roleCode)) {
authorities.add(new SimpleGrantedAuthority(permissionCode));
}
}*/
authorities.add(new SimpleGrantedAuthority("back:permission:save"));
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
这个也是要实现的进行用户密码校验和获取用户权限的方法,此方法中的实体类和service类我就不粘贴了,自己根据自己的需要写就可以了。用户的权限设计,我建议也采用官方的方式实现。
至此基本上就实现了一个简单的认证中心
资源微服务的实现,就一个配置文件就可以,如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
/**
* 资源服务配置
*/
@Configuration
@EnableResourceServer //该注解引入org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter拦截器
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/webjars/**","/*").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().csrf().disable();
}
// @Bean // 声明 ClientDetails实现
// @Primary
// public ClientDetailsService clientDetailsService() {
// return new JdbcClientDetailsService(dataSource);
// }
@Bean
@Primary
public TokenStore tokenStore() {
//使用内存的tokenStore
//return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
}
/**
* 判断来源请求是否包含oauth2授权信息
* url参数中含有access_token,或者header里有Authorization
*/
private static class OAuth2RequestedMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
// 请求参数中包含access_token参数
if (request.getParameter(OAuth2AccessToken.ACCESS_TOKEN) != null) {
return true;
}
// 头部的Authorization值以Bearer开头
String auth = request.getHeader("Authorization");
if (auth != null) {
return auth.startsWith(OAuth2AccessToken.BEARER_TYPE);
}
return false;
}
}
}
以上就是我总结的最近一段时间的学习内容,请指正
redis的实现TokenStore
public class OauthRedisTokenStore 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 OauthRedisTokenStore(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 connectionFactory.getConnection();
}
private byte[] serialize(Object object) {
return serializationStrategy.serialize(object);
}
private byte[] serializeKey(String object) {
return serialize(prefix + object);
}
private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
}
private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
return serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
}
private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
}
private byte[] serialize(String string) {
return serializationStrategy.serialize(string);
}
private String deserializeString(byte[] bytes) {
return serializationStrategy.deserializeString(bytes);
}
@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = authenticationKeyGenerator.extractKey(authentication);
byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
byte[] bytes = null;
RedisConnection conn = getConnection();
try {
bytes = conn.get(serializedKey);
} finally {
conn.close();
}
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
if (accessToken != null) {
OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
if ((storedAuthentication == null || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
// Keep the stores consistent (maybe the same user is
// represented by this authentication but the details have
// changed)
storeAccessToken(accessToken, authentication);
}
}
return accessToken;
}
@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return readAuthentication(token.getValue());
}
@Override
public OAuth2Authentication readAuthentication(String token) {
byte[] bytes = null;
RedisConnection conn = getConnection();
try {
bytes = conn.get(serializeKey(AUTH + token));
} finally {
conn.close();
}
OAuth2Authentication auth = deserializeAuthentication(bytes);
return auth;
}
@Override
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return 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) {
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
代码的出处忘了,找见出处后再加上来源