概念
什么是jwt,即 json web token。JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。也是一种token,但是和token有一些不同。默认情况下,OAuth2生成的token为自定义的UUID的token,里面没有用户信息,但是jwt中包含了用户信息,这就是两者的根本不同,以为oauth2的token是有状态的,oauth2生成的token与用户session关联,jwt的token中自包含用户信息,可以是无状态的,授权服务器不用记录jwt与session的关联,客户端请求的时候每次带上jwt令牌即可。
作用
JWT结构
JWT包含了使用.分割的三部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZV93YXlfc2VydmVyIl0sInVzZXJfaWQiOiIwMDEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInVzZXJfaW5mbyJdLCJleHAiOjE1OTY0MjQyNDYsImF1dGhvcml0aWVzIjpbIlJPTEVf5omA5pyJ5p2D6ZmQIl0sImp0aSI6Ijg2MGU0NDM5LWU3ZTItNDhjYy1hMWRkLWJiYWUwZjQ1M2RhMyIsImNsaWVudF9pZCI6Im15X2NsaWVudF9pZCJ9.2ANeby36jBtSo5BPlgVcs6QsCj62POIPtvdNCWbvI3c
为什么用jwt
正如我前面说到的,OAuth2自带生成的token是一个随机UUID,里面不带有任何与业务相关的信息,这样带来最大的问题,就是需要人工持久化处理token(保存)!!,但是jwt就不需要,因为自包含特点,所以token里有身份验证信息,不需要做后台持久化处理,前端每次请求被保护的资源时请求头里带上该token就可以实现。
前面的文章中,我给的都是授权服务认证成功后返回自带的token,本节,我将提供返回jwt的认证方式,其原理与之前差不多,仅仅是多了jwt的token转换器,将自带的token转换为jwt即可,如下配置我将一一进行讲解。
注意:
jwt自包含用户信息,不需要持久化token,所以不需要配置tokenStore和tokenService接口服务
授权服务配置
package com.easystudy.config;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
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.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
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.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import com.easystudy.oauth.UsernameUserDetailService;
/**
* @文件名称: AuthorizationServerConfig.java
* @功能描述: 授权服务配置类,配置token存储方式、clientDetail等
* @版权信息: www.easystudy.com
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @个人信息:941415509
* @开发日期: 2020年7月27日
* @历史版本: V1.0
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 用户认证管理器
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
// JDBC存储数据源
@Autowired
private DataSource dataSource;
// 用户权限接口实现
@Autowired
private UsernameUserDetailService userDetailsService;
// @Autowired
// private PasswordEncoder passwordEncoder;
/**
* @功能描述: 定义令牌端点上的安全性约 束
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @开发日期: 2020年8月3日
* @参数说明:
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()");
security.checkTokenAccess("isAuthenticated()");
security.allowFormAuthenticationForClients();
}
/**
* @功能描述: 用于定义客户端详细信息服务的配置程序。可以初始化客户端详细信息,也可以只引用现有
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @开发日期: 2020年8月3日
* @参数说明:
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置客户端:使用JDBC数据源加载用户信息
clients.jdbc(dataSource);
// 配置客户端:也可以自己加载后设置到内存中
// 配置客户端:自定义内存用户信息
// clients
// // 内存设置
// .inMemory()
// // 设置客户端id
// .withClient("my_client_id")
// // 设置客户端秘钥
// .secret(passwordEncoder.encode("my_client_secret"))
// // token失效时间(s)
// .accessTokenValiditySeconds(7 * 24 * 3600)
// // 刷新token有效时间
// .accessTokenValiditySeconds(14 * 24 * 3600)
// // 授权类型
// .authorizedGrantTypes("authorization_code","refresh_token","implicit","password","client_credentials")
// // 自动动过-无需用户确认
// .autoApprove(true)
// // 权限设置
// .authorities("user:add", "user:del")
// // 授权范围
// .scopes("read,writer");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 使用的认证管理器-默认包含登录认证、用户名密码认证
.authenticationManager(authenticationManager)
// 用户账号密码加载服务接口
.userDetailsService(userDetailsService)
// 增强策略-可以在JwtAccessTokenConverter中重载或传入
//.tokenEnhancer(jwtTokenEnhancer())
// 设置refresh token是否重复使用,若无,refresh_token会有UserDetailsService is required错误
.reuseRefreshTokens(false)
// 授权请求提交方式
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
// 配置JwtAccessToken转换器【重点】
.accessTokenConverter(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map additionalInformation = new HashMap<>();
additionalInformation.put("user_id", "001");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
return super.enhance(accessToken, authentication);
}
};
// 非对称加密,但jwt长度过长
// KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("auth2-jwt.jks"), "123456".toCharArray()).getKeyPair("auth2-jwt");
// converter.setKeyPair(keyPair);
// 对称加密
converter.setSigningKey("lixx");
return converter;
}
/**
* @功能描述: jwt身份转换器,用于将封装在jwt里面的信息解析或者转换
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @开发日期: 2020年8月3日
* @参数说明:
*/
public JwtAccessTokenConverter jwtTokenEnhancer() {
// RSA非对称加密密钥工厂,参数一为密钥地址,参数二为打开密钥的密码
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("auth2-jwt.jks"),"123456".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("auth2-jwt","123456".toCharArray()));
return converter;
}
}
这里与之前授权服务最大的区别在于,没有了tokenStore,也没有了DefaultTokenServices(实现了ResourceServerTokenServices),因为jwt自带用户信息,不需要授权服务进行持久化以及查询了!!所以看起来更加简洁。
这里主要配置一下几部分:
另外,还要注意的就是:
访问策略配置
package com.easystudy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import com.easystudy.oauth.UsernameUserDetailService;
/**
* @文件名称: WebSecurityConfig.java
* @功能描述: WebSecurity安全配置
* @版权信息: www.easystudy.com
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @联系方式: 941415509(QQ)
* @开发日期: 2020年7月26日
* @历史版本: V1.0
*/
@Order(2)
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UsernameUserDetailService usernameUserDetailService;
/**
* @功能描述: 认证管理器
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @开发日期: 2020年8月3日
* @参数说明:
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* @功能描述: 密码加密器
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @开发日期: 2020年8月3日
* @参数说明:
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* @功能描述: 受保护资源访问策略配置
* @编写作者: [email protected]
* @开发日期: 2020年7月26日
* @历史版本: V1.0
* @参数说明:
* @返 回 值:
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 资源访问安全策略
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
registry
// 任何配置都需要登录认证
.anyRequest()
.authenticated()
// 登录地址配置以及登录成功默认主页配置
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
// 登出接口权限配置
.and()
.logout().permitAll()
// 允许跨域请求
.and()
.csrf().disable()
// 进行http Basic认证
.httpBasic();
}
/**
* @功能描述: 用户读取位置以及密码验证方式
* @编写作者: [email protected]
* @开发日期: 2020年7月26日
* @历史版本: V1.0
* @参数说明:
* @返 回 值:
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// jwt无token存储
//auth.authenticationProvider(daoAuthenticationProvider());
auth
// 设置用户详情信息加载实现
.userDetailsService(usernameUserDetailService)
// 设置的密码加密器
.passwordEncoder(passwordEncoder());
}
/**
* @功能描述: 认证失败地址配置
* @编写作者: [email protected]
* @开发日期: 2020年7月26日
* @历史版本: V1.0
* @参数说明:
* @返 回 值:
*/
@Bean
public AuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
return new SimpleUrlAuthenticationFailureHandler("/login?error");
}
/**
* @功能描述: 静态资源忽略放行配置
* @编写作者: [email protected]
* @开发日期: 2020年7月26日
* @历史版本: V1.0
* @参数说明:
* @返 回 值:
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 放行静态资源,否则添加oauth2情况下无法显示
web.ignoring().antMatchers("/favor.ico", "/favicon.ico","/v2/api-docs", "/swagger-resources/configuration/ui",
"/swagger-resources","/swagger-resources/configuration/security",
"/swagger-ui.html","/css/**", "/js/**","/images/**", "/webjars/**", "**/favicon.ico", "/index");
}
}
这里,我们主要做了如下配置:
用户信息加载
package com.easystudy.oauth;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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 com.easystudy.model.Right;
import com.easystudy.service.RightService;
/**
* @文件名称: BaseUserDetailService.java
* @功能描述: 用户详情查询接口实现基类,提供基础的功能骨架,用户实现getUser接口提供用户查询实现即可(包括手机登录、扫码、微信登录等)
* @版权信息: www.dondown.com
* @编写作者: [email protected]
* @开发日期: 2020年4月8日
* @历史版本: V1.0
*/
public abstract class BaseUserDetailService implements UserDetailsService {
@Autowired
private RightService rightService; // 权限服务
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// 查找用户
com.easystudy.model.User user = getUser(userName);
if (null == user) {
throw new UsernameNotFoundException("用户:" + userName + ",不存在!");
}
// 设置用户权限
Set grantedAuthorities = new HashSet();
List rights = rightService.findByUsername(user.getUsername());
for(Right right : rights) {
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + right.getDescription());
grantedAuthorities.add(authority);
}
// 标识位设置
boolean enabled = user.getEnabled() == 0 ? false : true; // 可用性 :true:可用 false:不可用
boolean accountNonExpired = user.getExpired() == 0 ? true : false; // 过期性 :true:没过期 false:过期
boolean credentialsNonExpired = true; // 有效性 :true:凭证有效 false:凭证无效
boolean accountNonLocked = user.getLocked() == 0 ? true : false; // 锁定性 :true:未锁定 false:已锁定
return new User(user.getUsername(), user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
}
/**
* @功能描述: 用户信息查询抽象接口实现
* @编写作者: [email protected]
* @开发日期: 2020年4月8日
* @历史版本: V1.0
* @参数说明:
*/
protected abstract com.easystudy.model.User getUser(String var) ;
}
这里就是用户信息加载接口,实现了UserDetailsService,具体的用户名密码登录实现如下:
package com.easystudy.oauth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.easystudy.service.UserService;
/**
* @文件名称: UsernameUserDetailService.java
* @功能描述: 通过用户名查询用户信息
* @版权信息: www.easystudy.com
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @联系方式: 941415509(QQ)
* @开发日期: 2020年7月26日
* @历史版本: V1.0
*/
@Service
public class UsernameUserDetailService extends BaseUserDetailService {
@Autowired
private UserService userService;
@Override
protected com.easystudy.model.User getUser(String userName) {
return userService.findByUsername(userName);
}
}
相关数据库准备:
-- ------------------------------------------------------
-- 创建并使用数据库
-- ------------------------------------------------------
set charset utf8;
create database if not exists auth_demon character set UTF8;
use auth_demon;
-- ------------------------------------------------------
-- 创建授权认证中心表
-- ------------------------------------------------------
--
-- Table structure for table `oauth_client_details`
--
CREATE TABLE IF NOT EXISTS `oauth_client_details` (
`client_id` varchar(48) NOT NULL, /* 主键,必须唯一,不能为空. 用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成). 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念 */
`resource_ids` varchar(256) DEFAULT NULL, /* 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: “unity-resource,mobile-resource”. 该字段的值必须来源于与security.xml中标签‹oauth2:resource-server的属性resource-id值一致.
在security.xml配置有几个‹oauth2:resource-server标签, 则该字段可以使用几个该值. 在实际应用中, 我们一般将资源进行分类,并分别配置对应的‹oauth2:resource-server,如订单资源配置一个
‹oauth2:resource-server, 用户资源又配置一个‹oauth2:resource-server. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的注册流程,赋予对应的资源id. */
`client_secret` varchar(256) DEFAULT NULL, /* 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念. */
`scope` varchar(256) DEFAULT NULL, /* 指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: “read,write”. scope的值与security.xml中配置的‹intercept-url的access属性有关系. 如‹intercept-url
的配置为‹intercept-url pattern="/m / **" access=“ROLE_MOBILE,SCOPE_READ”/>则说明访问该URL时的客户端必须有read权限范围. write的配置值为SCOPE_WRITE, trust的配置值为SCOPE_TRUST.
在实际应该中, 该值一般由服务端指定, 常用的值为read,write. */
`authorized_grant_types` varchar(256) DEFAULT NULL, /* 指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔,如: “authorization_code,password”.
在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: “authorization_code,refresh_token”(针对通过浏览器访问的客户端);
“password,refresh_token”(针对移动设备的客户端). implicit与client_credentials在实际中很少使用 */
`web_server_redirect_uri` varchar(256) DEFAULT NULL, /* 客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:当grant_type=authorization_code时,
第一步 从 spring-oauth-server获取 'code’时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第二步 用 ‘code’ 换取 ‘access_token’ 时客户也必须传递相同
的redirect_uri. 在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值.在spring-oauth-client项目中, 可
具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法.当grant_type=implicit时通过redirect_uri的hash值来传递access_token值.
如:http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88-4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199然后客户端通过JS等从hash值中取到access_token值. */
`authorities` varchar(256) DEFAULT NULL, /* 指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_ */
`access_token_validity` int(11) DEFAULT NULL, /* 设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时). 在服务端获取的access_token JSON数据中的expires_in字段的值即为当前access_token的有效时间值.
在项目中, 可具体参考DefaultTokenServices.java中属性accessTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.refresh_token_validity 设定客户端的refresh_token的有效
时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属性
refreshTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义 */
`refresh_token_validity` int(11) DEFAULT NULL, /* 设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考
DefaultTokenServices.java中属性refreshTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义 */
`additional_information` varchar(4096) DEFAULT NULL, /* 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如:{“country”:“CN”,“country_code”:“086”}按照spring-security-oauth项目中对该字段的描述 Additional information
for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. (详见ClientDetails.java的getAdditionalInformation()方法的注释)
在实际应用中, 可以用该字段来存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等.create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) */
`autoapprove` varchar(256) DEFAULT NULL, /* 设置用户是否自动Approval操作, 默认值为 ‘false’, 可选值包括 ‘true’,‘false’, ‘read’,‘write’. 该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为’true’或支持的scope值,则会跳过用户Approve的页面,
直接授权. 该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性. 在项目中,主要操作oauth_client_details表的类是JdbcClientDetailsService.java, 更多的细节请参考该类. 也可以根据实际的需要,
去扩展或修改该类的实现. */
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `oauth_client_token`
--
CREATE TABLE IF NOT EXISTS `oauth_client_token` (
`token_id` varchar(128) DEFAULT NULL, /* 从服务器端获取到的access_token的值. */
`token` blob, /* 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据 */
`authentication_id` varchar(128) NOT NULL, /* 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultClientKeyGenerator.java类 */
`user_name` varchar(128) DEFAULT NULL, /* 登录时的用户名 */
`client_id` varchar(128) DEFAULT NULL, /* 客户appkey */
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `oauth_access_token`
--
CREATE TABLE IF NOT EXISTS `oauth_access_token` (
`token_id` varchar(128) DEFAULT NULL, /* 该字段的值是将access_token的值通过MD5加密后存储的 */
`token` blob, /* 存储将OAuth2AccessToken.java对象序列化后的二进制数据, 是真实的AccessToken的数据值 */
`authentication_id` varchar(128) NOT NULL, /* 该字段具有唯一性, 其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的 */
`user_name` varchar(128) DEFAULT NULL, /* 登录时的用户名, 若客户端没有用户名(如grant_type=“client_credentials”),则该值等于client_id */
`client_id` varchar(128) DEFAULT NULL, /* 平台注册的客户id */
`authentication` blob, /* 存储将OAuth2Authentication.java对象序列化后的二进制数据 */
`refresh_token` varchar(128) DEFAULT NULL, /* 该字段的值是将refresh_token的值通过MD5加密后存储的. 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java */
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `oauth_refresh_token`
--
CREATE TABLE IF NOT EXISTS `oauth_refresh_token` (
`token_id` varchar(128) NOT NULL, /* 该字段的值是将refresh_token的值通过MD5加密后存储的 */
`token` blob, /* 存储将OAuth2RefreshToken.java对象序列化后的二进制数据 */
`authentication` blob, /* 存储将OAuth2Authentication.java对象序列化后的二进制数据 */
PRIMARY KEY (`token_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `oauth_approvals`
--
CREATE TABLE IF NOT EXISTS `oauth_approvals` (
`userId` varchar(256) NOT NULL, /* 登录的用户名 */
`clientId` varchar(256) NOT NULL, /* 客户端ID */
`scope` varchar(256) DEFAULT NULL, /* 申请的权限 */
`status` varchar(10) DEFAULT NULL, /* 状态(Approve或Deny) */
`expiresAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, /* 过期时间 */
`lastModifiedAt` timestamp NOT NULL DEFAULT now(), /* 最终修改时间 */
PRIMARY KEY (`userId`, `clientId`) /* 自定义主键 */
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `oauth_code`
--
CREATE TABLE IF NOT EXISTS `oauth_code` (
-- `code` varchar(128) DEFAULT NULL, /* 存储服务端系统生成的code的值 */
`code` varchar(128) NOT NULL, /* 存储服务端系统生成的code的值 */
`authentication` blob, /* 存储将AuthorizationRequestHolder.java对象序列化后的二进制数据 */
PRIMARY KEY (`code`) /* 自定义添加主键 */
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- 插入用户授权秘钥测试记录:my_client_id秘钥值为明文为my_client_secret
--
LOCK TABLES `oauth_client_details` WRITE;
INSERT INTO `oauth_client_details` VALUES ('my_client_id','gate_way_server','$2a$10$9mmTWJd1pJ2OjWKG1G1pNuyUxIG6Lv8lic42VmBXYrVNG4ZB9FwL6','user_info','authorization_code,refresh_token,implicit,password,client_credentials','http://www.baidu.com','ROLE_ADMIN',7200,86400,'{\"systemInfo\":\"Atlas System\"}','true');
UNLOCK TABLES;
--
-- 创建权限表
--
CREATE TABLE IF NOT EXISTS t_right (
id int NOT NULL, /* 权限标识 */
name varchar(255) DEFAULT '', /* 权限名称 */
description varchar(64) DEFAULT '', /* 权限描述 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='system right';
--
-- 系统接口地址表
--
CREATE TABLE IF NOT EXISTS t_url (
id int NOT NULL, /* 权限明细标识 */
url varchar(256) DEFAULT '', /* 接口地址 */
description varchar(64) DEFAULT '', /* 接口描述 */
modify tinyint DEFAULT 0, /* 接口是否是更新操作 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='system url list';
--
-- 权限接口表
--
CREATE TABLE IF NOT EXISTS t_right_item (
id int NOT NULL AUTO_INCREMENT, /* 角色标识 */
rightId int NOT NULL, /* 权限标识 */
urlId int NOT NULL, /* 接口标识 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id),
foreign key(rightId) references t_right(id),
foreign key(urlId) references t_url(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='system url list';
--
-- 创建角色表
--
CREATE TABLE IF NOT EXISTS t_role (
id bigint(20) NOT NULL AUTO_INCREMENT, /* 角色标识 */
name varchar(64) DEFAULT '', /* 角色名称 */
description varchar(255) DEFAULT '', /* 角色描述 */
userType tinyint DEFAULT 1, /* 1:运营商,2学校,3机构,4教师,5家长 */
relativeId varchar(20) DEFAULT '', /* 关联学校或机构id */
createUser varchar(32) DEFAULT '', /* 创建用户名 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='system role';
--
-- 创建角色权限表
--
CREATE TABLE IF NOT EXISTS t_role_right (
id bigint(20) NOT NULL AUTO_INCREMENT, /* 表标识 */
roleId bigint(20) NOT NULL, /* 角色标识 */
rightId int DEFAULT NULL, /* 权限标识 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id),
foreign key(roleId) references t_role(id),
foreign key(rightId) references t_right(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='role right';
--
-- 创建用户表
--
CREATE TABLE IF NOT EXISTS t_user (
id bigint(20) NOT NULL AUTO_INCREMENT, /* 表标识 */
username varchar(32) DEFAULT '', /* 用户名 */
password varchar(255) DEFAULT '', /* 密码 */
mobile varchar(16) DEFAULT '', /* 手机 */
email varchar(32) DEFAULT '', /* 电子邮件 */
userType tinyint DEFAULT 1, /* 1:运营商,2学校,3机构,4教师,5家长 */
relativeId varchar(20) DEFAULT '', /* 关联学校或机构id */
head varchar(256) DEFAULT '', /* 用户头像 */
admin tinyint DEFAULT 0, /* 是否超级管理员 */
enabled tinyint DEFAULT 1, /* 可用性 */
expired tinyint DEFAULT 0, /* 是否过期 */
locked tinyint DEFAULT 0, /* 是否锁定 */
createUser varchar(32) DEFAULT '', /* 创建用户名 */
createTime datetime default now(), /* 创建时间 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='system user';
--
-- 创建用户角色表
--
CREATE TABLE IF NOT EXISTS t_user_role (
id bigint(20) NOT NULL AUTO_INCREMENT, /* 表标识 */
userId bigint(20) NOT NULL, /* 用户标识 */
roleId bigint(20) NOT NULL, /* 角色标识 */
reserver1 varchar(64) default NULL, /* 保留字段 */
reserver2 varchar(64) default NULL, /* 保留字段 */
primary key(id),
foreign key(userId) references t_user(id),
foreign key(roleId) references t_role(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='user role';
相关数据准备:
--
-- 插入用户授权秘钥测试记录:my_client_id秘钥值为明文为my_client_secret
--
LOCK TABLES `oauth_client_details` WRITE;
INSERT INTO `oauth_client_details` VALUES ('my_client_id','gate_way_server','$2a$10$9mmTWJd1pJ2OjWKG1G1pNuyUxIG6Lv8lic42VmBXYrVNG4ZB9FwL6','user_info','authorization_code,refresh_token,implicit,password,client_credentials','http://www.baidu.com','ROLE_ADMIN',7200,86400,'{\"systemInfo\":\"Atlas System\"}','true');
UNLOCK TABLES;
用户数据准备:
用户名: admin
密 码: 123456
这里,我们以密码模式进行登录获取jwt认证令牌信息。
请求地址:
http://localhost:7000/oauth/token?username=admin&password=123456&grant_type=password
请求方式: POST
请求结果:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZV93YXlfc2VydmVyIl0sInVzZXJfaWQiOiIwMDEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInVzZXJfaW5mbyJdLCJleHAiOjE1OTY0MjQyNDYsImF1dGhvcml0aWVzIjpbIlJPTEVf5omA5pyJ5p2D6ZmQIl0sImp0aSI6Ijg2MGU0NDM5LWU3ZTItNDhjYy1hMWRkLWJiYWUwZjQ1M2RhMyIsImNsaWVudF9pZCI6Im15X2NsaWVudF9pZCJ9.2ANeby36jBtSo5BPlgVcs6QsCj62POIPtvdNCWbvI3c",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZV93YXlfc2VydmVyIl0sInVzZXJfaWQiOiIwMDEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInVzZXJfaW5mbyJdLCJhdGkiOiI4NjBlNDQzOS1lN2UyLTQ4Y2MtYTFkZC1iYmFlMGY0NTNkYTMiLCJleHAiOjE1OTY1MDM0NDYsImF1dGhvcml0aWVzIjpbIlJPTEVf5omA5pyJ5p2D6ZmQIl0sImp0aSI6ImIwNWNlYjU2LTY5YmItNDZmZi05YzA3LWU5ZmMyZDRhNGNhOCIsImNsaWVudF9pZCI6Im15X2NsaWVudF9pZCJ9.U9BJqs2hry8Puo8N-zg6iFk6CZKxFhcfHJ3hklHwLEk",
"expires_in": 7199,
"scope": "user_info",
"user_id": "001",
"jti": "860e4439-e7e2-48cc-a1dd-bbae0f453da3"
}
返回结果可以在:https://jwt.io/ 官网上进行解密得到如下结果:
资源服务授权配置
当授权服务修改为JWT之后,token是不存储在授权服务的,所以资源服务不需要配置tokenInfoUri(通过token获取用户认证信息)或user-info-uri,这里直接配置上jwt授权服务器校验地址即可,如授权服务token改为jwt之后,资源服务配置可以修改如下所示:
#oauth2客户端
security:
oauth2:
#资源服务配置
resource:
#jwt验证地址
jwt:
#oauth2默认验证端点
key-uri: http://localhost:7000/oauth/token_key
#如下可暂时不用配置-仅做保留
client:
accessTokenUri: http://127.0.0.1:7000/oauth/token
userAuthorizationUri: http://127.0.0.1:7000/oauth/authorize
clientId: my_client_id
clientSecret: my_client_secret
资源服务配置
配置好授权服务校验地址之后,我们就可以配置资源服务了,资源服务与授权服务配置分离的情况下,资源服务配置如下:
package com.easystudy.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
/**
* @文件名称: ResourceServerConfiguration.java
* @功能描述: 资源服务访问配置
* @版权信息: www.easystudy.com
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @联系方式: 941415509(QQ)
* @开发日期: 2020年7月27日
* @历史版本: V1.0
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// 该资源服务器id必须在数据库记录中有配置,也就是对应token的用户必须该资源访问权限(密文:test_resource_secret)
// 例如,我的数据库记录:
// 'my_client_id','test_resource_id','$2a$10$I28j9B0T/roapkMEqfIHguARt0GgLyXwC/DOnFwPpXuQ0xTkrd632','user_info','authorization_code,refresh_token,implicit,password','http://localhost:7010/uaa/login','ROLE_ADMIN,ROLE_DEVICE,ROLE_VIDEO',3600,7200,'{\"systemInfo\":\"Atlas System\"}','true'
// 通过授权模式或简化模式获取的token(对应用户为wx_takeout_client_id)具有访问资源服务器test_resource_id
// 的权限,所以将该资源服务器id要与数据库的对应,否则无权访问
// 注意:在不使用代码配置的情况下资源服务器id默认值为: oauth2-resource
private static final String DEMO_RESOURCE_ID = "gate_way_server";
/**
* @功能描述: 以代码形式配置资源服务器id,配置文件配置不生效
* @编写作者: [email protected]
* @开发日期: 2020年7月27日
* @历史版本: V1.0
* @参数说明:
* @返 回 值:
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
}
/**
* 注意:从网关经过的所有url都进行过滤,情况分为如下两种:
* 1、带access_token的参数url,过滤器会获取参数到授权中心去鉴权
* 2、不带access_token的url,过滤器会获取本地‘资源服务’鉴权配置--即如下方法(或注解形式配置)
* 注意“**”的使用, 使用不好可能导致权限控制失效!!!(如果url前面无单词如/oauth/...,但是匹配路径用** /oauth,就会导致权限控制失效)
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// 其他匹配的[剩下的]任何请求都需要授权
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
registry
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.httpBasic();
}
}
**资源服务接口配置 **
我们提供资源服务接口,如下所示:
package com.easystudy.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
/**@文件名称: TestController.java
* @功能描述: TODO(用一句话描述该文件做什么)
* @版权信息: www.easystudy.com
* @技术交流: 961179337(QQ群)
* @编写作者: [email protected]
* @联系方式: 941415509(QQ)
* @开发日期: 2020年7月27日
* @历史版本: V1.0
*/
@RestController
@RequestMapping("/test")
@Api(value = "OAuth2 Client测试接口文档", tags = "OAuth2 Client测试接口文档")
public class TestController {
// oauth2注解
/**
* @RequiresUser:subject.isRemembered()结果为true,subject.isAuthenticated()
* @RequiresAuthentication:同于方法subject.isAuthenticated() 结果为true时
* @RequiresGuest:与@RequiresUser完全相反。
* @RequiresRoles("xx");有xx角色才可以访问方法
* @RequiresPermissions({"file:read", "write:aFile.txt"} ):同时含有file:read和write:aFile.txt的权限才能执行方法
*/
@GetMapping("/hi")
@ApiOperation(value="打招呼1", notes="打招呼1")
@ApiImplicitParams({ @ApiImplicitParam(paramType = "query", dataType = "String", name = "name", value = "名称", required = true) })
public String hi(@RequestParam(name = "name", required = true) String name){
return "hi " + name;
}
@GetMapping("/hello")
@ApiOperation(value="打招呼2", notes="打招呼2")
@ApiImplicitParams({ @ApiImplicitParam(paramType = "query", dataType = "String", name = "name", value = "名称", required = true) })
public String hello(@RequestParam(name = "name", required = true) String name){
return "hello " + name;
}
}
因为资源服务所有接口访问都需要认证(.anyRequest().authenticated())所以我们必须携带一个认证的token去访问资源服务,资源服务发现头部或查询参数携带access_token之后就会到授权服务进行token验证。所以我们需要通过postman先从授权服务获取一个token:
请求地址:
http://localhost:7000/oauth/token?username=admin&password=123456&grant_type=password
请求方式:POST
请求响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZV93YXlfc2VydmVyIl0sInVzZXJfaWQiOiIwMDEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInVzZXJfaW5mbyJdLCJleHAiOjE1OTY0MzIxMzMsImF1dGhvcml0aWVzIjpbIlJPTEVf5omA5pyJ5p2D6ZmQIl0sImp0aSI6IjNmODRkY2FjLTFmN2QtNDc4YS04ZjBjLTFiZTJmMGQ2MjJmZiIsImNsaWVudF9pZCI6Im15X2NsaWVudF9pZCJ9.6XyWQKVY3Hbs54u8S3G-J_yU1l2PqvX4DONHDQYfwKQ",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZV93YXlfc2VydmVyIl0sInVzZXJfaWQiOiIwMDEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInVzZXJfaW5mbyJdLCJhdGkiOiIzZjg0ZGNhYy0xZjdkLTQ3OGEtOGYwYy0xYmUyZjBkNjIyZmYiLCJleHAiOjE1OTY1MTEzMzMsImF1dGhvcml0aWVzIjpbIlJPTEVf5omA5pyJ5p2D6ZmQIl0sImp0aSI6IjI3ZTIxYWFkLWZhODAtNDU0YS1iMDk1LTk2ZWEyNDFjOTI0NyIsImNsaWVudF9pZCI6Im15X2NsaWVudF9pZCJ9.dfOYHtUUsNll3TPqTA9wGl5cS8hEmj6hXO1vfHD26Mk",
"expires_in": 7199,
"scope": "user_info",
"user_id": "001",
"jti": "3f84dcac-1f7d-478a-8f0c-1be2f0d622ff"
}
资源服务测试
有了token之后,我们就可以携带token访问资源服务了,访问地址:
http://localhost:7001/test/hi?name=lixx&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZ2F0ZV93YXlfc2VydmVyIl0sInVzZXJfaWQiOiIwMDEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInVzZXJfaW5mbyJdLCJleHAiOjE1OTY0MzIxMzMsImF1dGhvcml0aWVzIjpbIlJPTEVf5omA5pyJ5p2D6ZmQIl0sImp0aSI6IjNmODRkY2FjLTFmN2QtNDc4YS04ZjBjLTFiZTJmMGQ2MjJmZiIsImNsaWVudF9pZCI6Im15X2NsaWVudF9pZCJ9.6XyWQKVY3Hbs54u8S3G-J_yU1l2PqvX4DONHDQYfwKQ
访问资源服务,资源服务发现token并到授权服务验证,通过后,到达资源服务端点,返回结果:
@GetMapping("/hi")
@ApiOperation(value="打招呼1", notes="打招呼1")
@ApiImplicitParams({ @ApiImplicitParam(paramType = "query", dataType = "String", name = "name", value = "名称", required = true) })
public String hi(@RequestParam(name = "name", required = true) String name){
return "hi " + name;
}
以上就是授权服务配置jwt令牌以及资源服务响应做出调整的方案的全部内容,错误之处,敬请批评指正,谢谢!!
源码获取、合作、技术交流请获取如下联系方式:
微信账号:lixiang6153
公众号:IT技术快餐
电子邮箱:[email protected]