oauth2.0的使用
1 oauth2介绍
oauth是一个协议,它允许第三方网站无需获取用户的用户名和密码即可申请授权获取用户的资源信息,非常安全。比如我们常见的第三方登录功能就是使用了oauth。
2 oauth的表结构
官方的sql地址:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
-- used in tests that use HSQL(HSQL数据库的建表语句,如果使用mysql,需要将LONGVARBINARY类型换为longblob)
-- 这张表非常重要,客户端信息就存储在这张表中,当第三方客户端申请获取资源的时候,会根据请求中的client_id和client_secret
-- 来匹配这张表的记录,判断这个客户端能获取什么样的资源,有哪些权限,令牌的过期时间等等
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY, -- 客户端id
resource_ids VARCHAR(256), -- 资源id,这资源服务中设置的,表明可以访问该资源
client_secret VARCHAR(256), -- 客户端秘钥
scope VARCHAR(256), -- 指定客户端申请的权限范围,可选值包括read,write,trust
authorized_grant_types VARCHAR(256), -- 该客户端支持的授权方式 authorization_code,password,refresh_token,implicit,client_credentials,
web_server_redirect_uri VARCHAR(256), -- 授权码授权的回调地址
authorities VARCHAR(256), -- 指定客户端所拥有的Spring Security的权限值
access_token_validity INTEGER, -- 令牌的有效期,单位秒
refresh_token_validity INTEGER, -- 刷新令牌的有效期
additional_information VARCHAR(4096), -- 系统预留字段,一般不使用
autoapprove VARCHAR(256) -- 设置用户是否自动授权操作,授权码模式不弹出授权页面,而是登录之后直接返回授权码
);
create table oauth_client_token (
token_id VARCHAR(256),
token LONGVARBINARY,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
-- 当你在oauth配置类中指定令牌存储策略为JdbcTokenStore的时候,就会使用这张表
create table oauth_access_token (
token_id VARCHAR(256), -- 该字段的值是将access_token的值通过MD5加密后存储的
token LONGVARBINARY, -- 存储将OAuth2AccessToken.java对象序列化后的二进制数据, 是真实的AccessToken的数据值
authentication_id VARCHAR(256) PRIMARY KEY, -- 该字段具有唯一性, 其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultAuthenticationKeyGenerator.java类.
user_name VARCHAR(256), -- 登录时的用户名, 若客户端没有用户名(如grant_type=“client_credentials”),则该值等于client_id
client_id VARCHAR(256),
authentication LONGVARBINARY, -- 存储将OAuth2Authentication.java对象序列化后的二进制数据
refresh_token VARCHAR(256) -- 该字段的值是将refresh_token的值通过MD5加密后存储的. 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java
);
-- 当你在oauth配置类中指定令牌存储策略为JdbcTokenStore的时候,就会使用这张表
create table oauth_refresh_token (
token_id VARCHAR(256), -- 该字段的值是将refresh_token的值通过MD5加密后存储的.
token LONGVARBINARY, -- 存储将OAuth2RefreshToken.java对象序列化后的二进制数据
authentication LONGVARBINARY -- 存储将OAuth2Authentication.java对象序列化后的二进制数据
);
-- 当你在oauth配置类中指定授权码模式专用对象策略为JdbcAuthorizationCodeServices的时候,就会使用这张表
create table oauth_code (
code VARCHAR(256), -- 存储服务端系统生成的code的值(未加密)
authentication LONGVARBINARY -- 存储将AuthorizationRequestHolder.java对象序列化后的二进制数据
);
-- 当你使用授权码模式授权之后,就会在这张表中保存授权信息
create table oauth_approvals (
userId VARCHAR(256), --用户名,username
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10), -- 授权状态 APPROVED 已授权
expiresAt TIMESTAMP, -- 第一次请求授权时间
lastModifiedAt TIMESTAMP -- -- 上一次请求授权时间
);
-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
3 oauth2的认证服务
3.1 关键包
<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.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
3.2 导入进行security认证的类
domain
@Data
@Table(name = "sys_user")
public class SysUser implements UserDetails {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "status")
private Integer status;
private List<SysPermission> authorities;
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return status==1;
}
}
@Data
@Table(name = "sys_permission")
public class SysPermission implements GrantedAuthority {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "ID")
private Integer id;
@Column(name = "permission_NAME")
private String permissionName;
@Column(name = "permission_url")
private String permissionUrl;
@Column(name = "parent_id")
private String parentId;
/**
* If the <code>GrantedAuthority</code> can be represented as a <code>String</code>
* and that <code>String</code> is sufficient in precision to be relied upon for an
* access control decision by an {@link AccessDecisionManager} (or delegate), this
* method should return such a <code>String</code>.
* <p>
* If the <code>GrantedAuthority</code> cannot be expressed with sufficient precision
* as a <code>String</code>, <code>null</code> should be returned. Returning
* <code>null</code> will require an <code>AccessDecisionManager</code> (or delegate)
* to specifically support the <code>GrantedAuthority</code> implementation, so
* returning <code>null</code> should be avoided unless actually required.
*
* @return a representation of the granted authority (or <code>null</code> if the
* granted authority cannot be expressed as a <code>String</code> with sufficient
* precision).
*/
@JsonIgnore
@Override
public String getAuthority() {
return permissionName;
}
}
dao
public interface UserMapper extends Mapper<SysUser> {
@Select("select * from sys_user where username=#{username}")
@Results({@Result(id = true, property = "id", column = "id"),
@Result(property = "authorities", column = "id", javaType = List.class,
many = @Many(select = "cn.lx.security.dao.RermissionMapper.findByUid"))})
public SysUser findByUsername(String username);
}
public interface PermissionMapper extends Mapper<SysPermission> {
@Select("SELECT * FROM sys_permission WHERE ID IN(" +
"SELECT PID FROM sys_role_permission WHERE RID IN(" +
"SELECT RID FROM sys_user_role WHERE uid=#{uid}" +
"))")
public List<SysPermission> findByUid(Integer uid);
}
service
public interface IUserService extends UserDetailsService {
}
@Service
public class IUserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userMapper.findByUsername(username);
if (null==sysUser){
throw new RuntimeException("没有此用户");
}
return sysUser;
}
}
3.3 application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql:///security_oauth?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
main:
allow-bean-definition-overriding: true
mybatis:
type-aliases-package: cn.lx.security.domain
configuration:
#驼峰
map-underscore-to-camel-case: true
3.4 security的配置
security的配置和以前差不多
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IUserService userService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 只有这个配置类有AuthenticationManager对象,我们要把这个类中的这个对象放入容器中
* 这样在别的地方就可以自动注入了
* oauth中需要用
* @return
* @throws Exception
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* Used by the default implementation of {@link #authenticationManager()} to attempt
* to obtain an {@link AuthenticationManager}. If overridden, the
* {@link AuthenticationManagerBuilder} should be used to specify the
* {@link AuthenticationManager}.
*
* <p>
* The {@link #authenticationManagerBean()} method can be used to expose the resulting
* {@link AuthenticationManager} as a Bean. The {@link #userDetailsServiceBean()} can
* be used to expose the last populated {@link UserDetailsService} that is created
* with the {@link AuthenticationManagerBuilder} as a Bean. The
* {@link UserDetailsService} will also automatically be populated on
* {@link HttpSecurity#getSharedObject(Class)} for use with other
* {@link SecurityContextConfigurer} (i.e. RememberMeConfigurer )
* </p>
*
* <p>
* For example, the following configuration could be used to register in memory
* authentication that exposes an in memory {@link UserDetailsService}:
* </p>
*
* <pre>
* @Override
* protected void configure(AuthenticationManagerBuilder auth) {
* auth
* // enable in memory based authentication with a user named
* // "user" and "admin"
* .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
* .withUser("admin").password("password").roles("USER", "ADMIN");
* }
*
* // Expose the UserDetailsService as a Bean
* @Bean
* @Override
* public UserDetailsService userDetailsServiceBean() throws Exception {
* return super.userDetailsServiceBean();
* }
*
* </pre>
*
* @param auth the {@link AuthenticationManagerBuilder} to use
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder());
}
/**
* Override this method to configure {@link WebSecurity}. For example, if you wish to
* ignore certain requests.
*
* @param web
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico");
}
/**
* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their
* configuration. The default configuration is:
*
* <pre>
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
* </pre>
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception if an error occurs
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
//这个不需要,oauth已经自动开放了
//.and()
//.authorizeRequests().antMatchers("/oauth/token/**").permitAll()
.and()
.formLogin().loginProcessingUrl("/login").permitAll();
//.and()
// .logout().logoutUrl("/logout").invalidateHttpSession(true).permitAll();
}
}
3.5 oauth2的配置
配置类记住一个注解@EnableAuthorizationServer
,注解源码会告诉你如何使用
/**
* Convenience annotation for enabling an Authorization Server (i.e. an {@link AuthorizationEndpoint} and a
* {@link TokenEndpoint}) in the current application context, which must be a {@link DispatcherServlet} context. Many
* features of the server can be customized using <code>@Beans</code> of type {@link AuthorizationServerConfigurer}
* (e.g. by extending {@link AuthorizationServerConfigurerAdapter}). The user is responsible for securing the
* Authorization Endpoint (/oauth/authorize) using normal Spring Security features ({@link EnableWebSecurity
* @EnableWebSecurity} etc.), but the Token Endpoint (/oauth/token) will be automatically secured using HTTP Basic
* authentication on the client's credentials. Clients <em>must</em> be registered by providing a
* {@link ClientDetailsService} through one or more AuthorizationServerConfigurers.
*
* @author Dave Syer
*
*/
这段话重点就是说,配置类需要继承AuthorizationServerConfigurerAdapter,其他的后面说
首先看看AuthorizationServerConfigurerAdapter源码
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
/**
*配置令牌端点的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
/**
*配置客户端详细信息的来源
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
/**
*配置令牌的访问端点和令牌服务
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
3.5.1 配置客户端详细信息的来源
3.5.1.1 内存中存储客户端详细信息
/**
* 连接数据库,查询客户端信息
* 配置客户端详细信息
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//内存中设置一个客户端
clients
.inMemory()
.withClient("heima_one")
.secret(bCryptPasswordEncoder.encode("123456"))
.scopes("read")
.authorizedGrantTypes("password")
.redirectUris("http://www.baidu.com");
}
3.5.1.2 mysql数据库存储客户端详细信息
(1)使用ClientDetailsService来获取数据库中的信息
/**
* 连接数据库获取客户端信息
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
/**
* 连接数据库,查询客户端信息
* 配置客户端详细信息
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//连接数据库获取客户端信息
clients.withClientDetails(clientDetailsService());
}
(2)直接使用jdbc连接数据库查询
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* 连接数据库,查询客户端信息
* 配置客户端详细信息
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//连接数据库获取客户端信息
clients.jdbc(dataSource).passwordEncoder(bCryptPasswordEncoder);
}
3.5.2 配置令牌的访问端点和令牌服务
3.5.2.1 设置认证管理器
这个没什么好说的,在security的配置类中取出放入spring容器中
3.5.2.2 设置授权信息存储策略
可以设置两种策略
(1)保存在内存中
/**
* 授权信息保存策略
* @return
*/
@Bean
public ApprovalStore approvalStore(){
return new InMemoryApprovalStore();
}
(2)保存在数据库中
@Autowired
private DataSource dataSource;
/**
* 授权信息保存策略
* @return
*/
@Bean
public ApprovalStore approvalStore(){
return new JdbcApprovalStore(dataSource);
}
3.5.2.3 设置令牌存储策略
oauth默认是生成普通令牌的
生成策略有三种,存在内存中和数据库中差不多,这两种返回的令牌中,并不包含信息,他就是一个唯一标识,通过它,服务器可以在内存中或者数据库中找到用户信息。jwt则是直接将信息保存在令牌中
(1)存储在内存中
/**
* token保存策略
*
* @return 返回一个用于生成和检索令牌的对象
*/
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
(2)存储在内存中
/**
* token保存策略
*
* @return 返回一个用于生成和检索令牌的对象
*/
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
(3)使用jwt
jwt对比上述两种稍微麻烦了一点,我们除了需要配置token保存策略之外,还要配置一个通行令牌转换器
/**
* 读取application.yml中配置的证书文件,需要设置位置,密码,别名等
* @return
*/
@Bean
public KeyProperties keyProperties() {
KeyProperties keyProperties = new KeyProperties();
return keyProperties;
}
/**
* token保存策略
*
* @return 返回一个用于生成和检索令牌的对象
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 设置令牌转换器
* 令牌生成方式
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//这种方式传入私钥一直报错,不知道啥原因,所以现在改为使用秘钥库文件获取私钥
//String privateKey = KeyUtil.readKey("privateKey.txt");
KeyPair keyPair = new KeyStoreKeyFactory(
//将秘钥库文件转化为resource
keyProperties().getKeyStore().getLocation()
//秘钥库文件的访问密码
, keyProperties().getKeyStore().getPassword().toCharArray())
//获取秘钥对
.getKeyPair(keyProperties().getKeyStore().getAlias());
//设置秘钥对
converter.setKeyPair(keyPair);
return converter;
}
还需要将这两个对象设置到oauth中才能生效
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//设置认证管理器,通过它验证用户名和密码是否正确
.authenticationManager(authenticationManager)
//设置授权信息存储策略,可以选择内存或者数据库
.approvalStore(approvalStore())
//设置令牌存储策略,可以选择普通令牌或者jwt令牌
.tokenStore(tokenStore())
//设置令牌转换器,指定生成jwt格式令牌,令牌存储策略为jwt型必须设置该配置才能生效
.accessTokenConverter(jwtAccessTokenConverter())
//设置授权码认证专用对象,可以选择将授权码保存在内存或者内存
.authorizationCodeServices(authorizationCodeServices());
}
application.yml中对应的配置如下
encrypt:
key-store:
alias: testoauth
location: classpath:/testoauth.jks
password: testoauth
secret: testoauth
testoauth.jks这是秘钥证书,如何得到,我的另一篇文档上面有教程
3.5.2.4 设置授权码认证专用对象
可以设置授权码保存策略
(1)内存
/**
* 授权码保存策略
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
(2)数据库
/**
* 授权码保存策略
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
3.5.2.5 设置令牌服务
(1)可以通过数据库中客户端信息表指定令牌的配置
不指定令牌服务就会使用默认的配置
(2)也可以创建一个自定义的令牌服务,将令牌的配置写死
这里可以指定令牌的一些设置,先看一下默认令牌服务创建的令牌的设置的源码
private DefaultTokenServices createDefaultTokenServices() {
//创建一个令牌服务
DefaultTokenServices tokenServices = new DefaultTokenServices();
//设置令牌存储策略
tokenServices.setTokenStore(tokenStore());
//设置支持刷新令牌
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
//设置客户端信息来源
tokenServices.setClientDetailsService(clientDetailsService());
tokenServices.setTokenEnhancer(tokenEnhancer());
//设置用户认证服务
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}
我们按照这个源码自定义一个令牌服务
/**
* 自定义一个令牌服务
* @return
*/
@Bean
public DefaultTokenServices myTokenServices(){
//创建一个令牌服务
DefaultTokenServices tokenServices = new DefaultTokenServices();
//设置令牌存储策略
tokenServices.setTokenStore(tokenStore());
//设置支持刷新令牌
tokenServices.setSupportRefreshToken(true);
//设置客户端信息来源
tokenServices.setClientDetailsService(clientDetailsService());
//设置令牌过期时间2小时
tokenServices.setAccessTokenValiditySeconds(7200);
//刷新令牌默认有效期3天
tokenServices.setRefreshTokenValiditySeconds(259200);
return tokenServices;
}
3.5.2.6 将上述配置设置到oauth中
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//设置认证管理器,通过它验证用户名和密码是否正确
.authenticationManager(authenticationManager)
//设置授权信息存储策略,可以选择内存或者数据库
.approvalStore(approvalStore())
//设置令牌存储策略,可以选择普通令牌或者jwt令牌
.tokenStore(tokenStore())
//设置令牌转换器,指定生成jwt格式令牌,令牌存储策略为jwt型必须设置该配置才能生效
.accessTokenConverter(jwtAccessTokenConverter())
//设置授权码认证专用对象,可以选择将授权码保存在内存或者内存
.authorizationCodeServices(authorizationCodeServices())
//设置令牌服务,这里设置令牌的过期时间等等
.tokenServices(myTokenServices());
}
3.5.3 配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//端点/oauth/token_key: 提供公有秘钥的端点,如果使用jwt令牌的话
.tokenKeyAccess("permitAll()")
//端点/oauth/check_token: 用于资源服务令牌解析
.checkTokenAccess("permitAll()")
//允许通过表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
3.5.4 oauth2完整的配置
这里的配置是生成jwt格式令牌
@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 读取application.yml中配置的证书文件,需要设置位置,密码,别名等
* @return
*/
@Bean
public KeyProperties keyProperties() {
KeyProperties keyProperties = new KeyProperties();
return keyProperties;
}
@Autowired
private DataSource dataSource;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* 用来查找用户的,而非客户端
*/
@Autowired
private AuthenticationManager authenticationManager;
/**
* 连接数据库获取客户端信息
*
* @return
*/
@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* token保存策略
*
* @return 返回一个用于生成和检索令牌的对象
*/
@Bean
public TokenStore tokenStore() {
//return new InMemoryTokenStore();
//return new JdbcTokenStore(dataSource);
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 设置令牌转换器
* 令牌生成方式
*
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//这种方式传入私钥一直报错,不知道啥原因,所以现在改为使用秘钥库文件获取私钥
//String privateKey = KeyUtil.readKey("privateKey.txt");
KeyPair keyPair = new KeyStoreKeyFactory(
//将秘钥库文件转化为resource
keyProperties().getKeyStore().getLocation()
//秘钥库文件的访问密码
, keyProperties().getKeyStore().getPassword().toCharArray())
//获取秘钥对
.getKeyPair(keyProperties().getKeyStore().getAlias());
//设置秘钥对
converter.setKeyPair(keyPair);
return converter;
}
/**
* 授权信息保存策略
*
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
//return new InMemoryApprovalStore();
}
/**
* 授权码保存策略
*
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
//return new InMemoryAuthorizationCodeServices();
}
/**
* 自定义一个令牌服务
*
* @return
*/
/*@Bean
public DefaultTokenServices myTokenServices(){
//创建一个令牌服务
DefaultTokenServices tokenServices = new DefaultTokenServices();
//设置令牌存储策略
tokenServices.setTokenStore(tokenStore());
//设置支持刷新令牌
tokenServices.setSupportRefreshToken(true);
//设置客户端信息来源
tokenServices.setClientDetailsService(clientDetailsService());
//设置令牌过期时间2小时
tokenServices.setAccessTokenValiditySeconds(7200);
//刷新令牌默认有效期3天
tokenServices.setRefreshTokenValiditySeconds(259200);
return tokenServices;
}
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//端点/oauth/token_key:提供公有秘钥的端点,如果使用jwt令牌的话
.tokenKeyAccess("permitAll()")
//端点/oauth/check_token: 用于资源服务令牌解析
.checkTokenAccess("permitAll()")
//允许通过表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
/**
* 连接数据库,查询客户端信息
* 配置客户端详细信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//内存中设置一个客户端
// clients
// .inMemory()
// .withClient("heima_one")
// .secret(bCryptPasswordEncoder.encode("123456"))
// .scopes("read")
// .authorizedGrantTypes("password")
// .redirectUris("http://www.baidu.com");
//连接数据库获取客户端信息
//clients.jdbc(dataSource).passwordEncoder(bCryptPasswordEncoder);
clients.withClientDetails(clientDetailsService());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//设置认证管理器,通过它验证用户名和密码是否正确
.authenticationManager(authenticationManager)
//设置授权信息存储策略,可以选择内存或者数据库
.approvalStore(approvalStore())
//设置令牌存储策略,可以选择普通令牌或者jwt令牌
.tokenStore(tokenStore())
//设置令牌转换器,指定生成jwt格式令牌,令牌存储策略为jwt型必须设置该配置才能生效
.accessTokenConverter(jwtAccessTokenConverter())
//设置授权码认证专用对象,可以选择将授权码保存在内存或者内存
.authorizationCodeServices(authorizationCodeServices());
//设置令牌服务,这里设置令牌的过期时间等等
//.tokenServices(myTokenServices());
//.userDetailsService(userDetailsService);
}
}
4 资源服务
4.1 security的配置
security的配置和以前差不多
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their
* configuration. The default configuration is:
*
* <pre>
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
* </pre>
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception if an error occurs
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
//禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
}
4.2 oauth2的配置
//记住这个注解,继承的类从注解源码注释找
@EnableResourceServer
继承ResourceServerConfigurerAdapter,下面是他的源码
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
/**
* 设置资源标识,并设置令牌验证策略
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
}
/**
* 和security中的同名方法差不多
* 这里面可以配置客户端的访问权限
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
4.2.1 配置令牌验证策略
我们需要告诉资源服务器令牌的类型,还有如何验证令牌是否有效
/**
* 告诉服务器令牌的类型是jwt
* @return
*/
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 告诉服务器令牌如何验证
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
String publicKey = KeyUtil.readKey("publicKey.txt");
//设置rsa公钥
jwtAccessTokenConverter.setVerifierKey(publicKey);
return jwtAccessTokenConverter;
}
4.2.2 设置资源标识,并设置令牌验证策略
/**
* 设置资源标识,并设置令牌验证策略
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
//很重要,唯一标识这个资源服务器
.resourceId("product_api")
.tokenStore(tokenStore());
}
4.2.3 配置客户端的访问权限
/**
* 和security中的同名方法差不多
* 这里面可以配置客户端的访问权限
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
//前后端分离,禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
//客户端权限为read才能使用get方法,读取资源
.antMatchers(HttpMethod.GET).access("#oauth2.hasScope('read')")
//客户端权限为write能使用post方法,修改资源
.antMatchers(HttpMethod.POST).access("#oauth2.hasScope('write')");
}
4.3 oauth的完整配置
@Configuration
//记住这个注解,继承的类从注解源码注释找
@EnableResourceServer
public class OauthResourceConfig extends ResourceServerConfigurerAdapter {
/**
* 告诉服务器令牌的类型是jwt
* @return
*/
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 告诉服务器令牌如何验证
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
String publicKey = KeyUtil.readKey("publicKey.txt");
//设置rsa公钥
jwtAccessTokenConverter.setVerifierKey(publicKey);
return jwtAccessTokenConverter;
}
/**
* 设置资源标识,并设置令牌验证策略
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
//很重要,唯一标识这个资源服务器
.resourceId("product_api")
.tokenStore(tokenStore());
}
/**
* 和security中的同名方法差不多
* 这里面可以配置客户端的访问权限
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
//前后端分离,禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
//客户端权限为read才能使用get方法,读取资源
.antMatchers(HttpMethod.GET).access("#oauth2.hasScope('read')")
//客户端权限为write能使用post方法,修改资源
.antMatchers(HttpMethod.POST).access("#oauth2.hasScope('write')");
}
}
5 自定义令牌内容
系统自带的jwt令牌生成(JwtAccessTokenConverter),令牌中携带的内容是固定的,如果我们想向里面添加内容的话,我们就重写
JwtAccessTokenConverter的中令牌生成方法
原始的jwt令牌携带的内容,类似如下面
{
"aud": [
"product_api"
],
"user_name": "wx",
"scope": [
"read"
],
"active": true,
"exp": 1602429722,
"authorities": [
"PRODUCT_LIST"
],
"jti": "7489afd8-947e-488b-ad6e-d27df456777d",
"client_id": "test_one"
}
5.1 令牌生成源码分析
接下来分析一下源码,看看令牌是如何生成的
public class JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean {
/**
* Field name for token id.
*/
public static final String TOKEN_ID = AccessTokenConverter.JTI;
/**
* Field name for access token id.
*/
public static final String ACCESS_TOKEN_ID = AccessTokenConverter.ATI;
private static final Log logger = LogFactory.getLog(JwtAccessTokenConverter.class);
//看这里,它定义了一个令牌转换器,下面生成令牌的方法用到了它
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
private JwtClaimsSetVerifier jwtClaimsSetVerifier = new NoOpJwtClaimsSetVerifier();
private JsonParser objectMapper = JsonParserFactory.create();
private String verifierKey = new RandomValueStringGenerator().generate();
private Signer signer = new MacSigner(verifierKey);
private String signingKey = verifierKey;
private SignatureVerifier verifier;
/**
* 看这个方法名估计这个就是生成令牌的方法了
*/
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
//获取令牌的内容
content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert access token to JSON", e);
}
//生成令牌
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
}
//后面的省略
}
看DefaultAccessTokenConverter的convertAccessToken方法的源码
public class DefaultAccessTokenConverter implements AccessTokenConverter {
private UserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
private boolean includeGrantType;
private String scopeAttribute = SCOPE;
private String clientIdAttribute = CLIENT_ID;
//省略了其他方法,只关注我们需要的
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
Map<String, Object> response = new HashMap<String, Object>();
OAuth2Request clientToken = authentication.getOAuth2Request();
//这个方法的意思是判断令牌是否绑定了一个用户,如果不理解,就去看一下oauth的客户端模式
if (!authentication.isClientOnly()) {
//其他模式,放入用户相关信息。我们就看这个方法,这个方法里面肯定向response添加用户名和权限,因为到目前为止,令牌的其他属性我们都在这里找见了,可是用户名呢?
response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
} else {
//客户端模式,放入客户端权限
if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) {
response.put(UserAuthenticationConverter.AUTHORITIES,
AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
}
}
//下面是放入令牌的公共属性,是不是觉得很熟悉,就是上面令牌的属性嘛
if (token.getScope()!=null) {
response.put(scopeAttribute, token.getScope());
}
if (token.getAdditionalInformation().containsKey(JTI)) {
response.put(JTI, token.getAdditionalInformation().get(JTI));
}
if (token.getExpiration() != null) {
response.put(EXP, token.getExpiration().getTime() / 1000);
}
if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) {
response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType());
}
response.putAll(token.getAdditionalInformation());
response.put(clientIdAttribute, clientToken.getClientId());
if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {
response.put(AUD, clientToken.getResourceIds());
}
return response;
}
}
看DefaultUserAuthenticationConverter的convertUserAuthentication方法的源码
public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
private Collection<? extends GrantedAuthority> defaultAuthorities;
private UserDetailsService userDetailsService;
/**
* 看到这个方法是不是明白了,用户相关信息是在这个封装进去的,那我们要添加自定义信息就只需要继承这个类,重写此方法了
*/
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<String, Object>();
//加入用户名
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
//加入用户权限
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
5.2 重写convertUserAuthentication方法,向令牌中添加自定义信息
@Component
public class MyJwtUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
/**
* 向令牌添加自定义信息
* @param authentication
* @return
*/
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<String, Object>();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
//添加一个sex
response.put("sex","nan");
return response;
}
}
5.3 使重写的方法生效
只需要在oauth配置类中定义令牌转换器的时候,将我们自定义的MyJwtUserAuthenticationConverter设置进去就好了
/**
* 设置令牌转换器
* 令牌生成方式
*
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//这种方式传入私钥一直报错,不知道啥原因,所以现在改为使用秘钥库文件获取私钥
//String privateKey = KeyUtil.readKey("privateKey.txt");
KeyPair keyPair = new KeyStoreKeyFactory(
//将秘钥库文件转化为resource
keyProperties().getKeyStore().getLocation()
//秘钥库文件的访问密码
, keyProperties().getKeyStore().getPassword().toCharArray())
//获取秘钥对
.getKeyPair(keyProperties().getKeyStore().getAlias());
//设置秘钥对
converter.setKeyPair(keyPair);
//获取jwt默认的令牌转化工具,DefaultAccessTokenConverter是其真实的类型
DefaultAccessTokenConverter defaultAccessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
//自定义的令牌生成格式覆盖原始的
defaultAccessTokenConverter.setUserTokenConverter(myJwtUserAuthenticationConverter);
return converter;
}
5.4 测试
6 自定义令牌颁发
在某些时候令牌无法传递,比如说在进行feign调用的时候,我们一般是配置一个feign请求的拦截器,然后在feign请求的请求头中添加一块令牌,让feign能获取到它请求的资源,我们一般是自己生成一块具有管理员权限的令牌,但是要将令牌的过期时间设置的短一点,防止有人窃取了该令牌,那么现在就来说说如何生成一块令牌。
在上面已经详细解释过了系统颁发的令牌的生成过程,那我们现在就模仿下一,首先看源码
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
//令牌的主体内容,也就是载荷
String content;
try {
//这个方法是载荷的封装
content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert access token to JSON", e);
}
//这里就是根据载荷生成一块令牌了
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
}
接下来再来看看令牌的载荷有哪些
{
"aud": [
"product_api"
],
"user_name": "wx",
"scope": [
"read"
],
"active": true,
"exp": 1602429722,
"authorities": [
"PRODUCT_LIST"
],
"jti": "7489afd8-947e-488b-ad6e-d27df456777d",
"client_id": "test_one"
}
aud:资源id,这个得有,你要获取哪个资源,就写哪个资源id
user_name:用户名
scope:客户端权限,这里面应该把所有权限都给它
exp:令牌的失效时间,单位是秒
authorities:用户权限,给管理员权限
client_id:客户端id
我们就只需要这几个
@SpringBootTest
@RunWith(SpringRunner.class)
public class OauthJwtTest {
@Autowired
private KeyProperties keyProperties;
/**
* 创建一个令牌
* @return
*/
@Test
public void createJwt(){
KeyPair keyPair = new KeyStoreKeyFactory(
//将秘钥库文件转化为resource
keyProperties.getKeyStore().getLocation()
//秘钥库文件的访问密码
, keyProperties.getKeyStore().getPassword().toCharArray())
//获取秘钥对
.getKeyPair(keyProperties.getKeyStore().getAlias());
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String,Object> content=new HashMap<>(16);
//该令牌内容
content.put("authorities",new String[]{"PRODUCT_LIST"});
content.put("aud",new String[]{"product_api"});
content.put("scope",new String[]{"read"});
content.put("client_id","heima_one");
content.put("user_name","lx");
//设置超时时间为当前时间的100秒之后,需要改为秒
content.put("exp",System.currentTimeMillis()/1000+100);
String token = JwtHelper.encode(JSON.toJSONString(content), new RsaSigner(privateKey)).getEncoded();
System.out.println(token);
}
}
进行令牌校验,看看我们的令牌能否被oauth识别
多了一项数据,sex:“男”,这个我们没有向载荷封装啊,他是怎么出来的,我们跟踪一下源码
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
//对令牌的合法性进行校验
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
//这个方法是不是很熟悉,我们自定义令牌内容的时候分析过,他会调用我们重写的那个向令牌添加自定义信息的方法
Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);
// gh-1070
response.put("active", true); // Always true if token exists and not expired
return response;
}
校验不是简单的验签之后对令牌载荷base64解码