搭建授权服务器oauth2
一、授权服务器的定位
一言而概之:就是为客户端产生一个Token
如图所示:
二、授权服务器的实现
2.1 添加依赖
<!-- 服务发现--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>2.2.6.RELEASE</verison></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.6.RELEASE</verison></dependency>
2.2 配置文件
spring:application:name: authorization-servercloud:nacos:discovery:server-addr: nacos-server:8848
server:port: 9999
2.3 启动类
package com.zhubayi.authorization;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** @author zhubayi*/
@SpringBootApplication
@EnableDiscoveryClient
public class AuthorizationApplication {public static void main(String[] args) {SpringApplication.run(AuthorizationApplication.class,args);}}
2.4配置类
2.4.1 授权服务器的配置
@EnableAuthorizationServer
@Configuration
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {@Autowiredpublic PasswordEncoder passwordEncoder ;@Autowiredprivate AuthenticationManager authenticationManager ;@Autowiredprivate UserDetailsService userDetailsService ;/*** 配置第三方客户端*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("coin-api") //客户端id.secret(passwordEncoder.encode("coin-secret")) //客户端密码,要加密,不然一直要求登录,获取不到临牌,一定不要泄露.scopes("all")//授权范围标识,哪部分资源可访问(all是标识,不是代表所有).authorizedGrantTypes("password","refresh_token")//refresh_token配置这个才能刷新令牌 grant_type对应 password.autoApprove(false)//false 跳转到授权页面手动点击,true不用手动点击,直接响应授权.accessTokenValiditySeconds(24 * 3600) //24小时.refreshTokenValiditySeconds(7 * 24 * 3600);//七天}/*** 设置授权管理器和UserDetailsService 内存认证* @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(new InMemoryTokenStore()).authenticationManager(authenticationManager).userDetailsService(userDetailsService) ;}
}
2.4.2 Web 安全的配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 注入一个验证管理器** @return* @throws Exception*/@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 资源的放行*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable(); // 关闭scrf 跨域http.authorizeRequests().anyRequest().authenticated();}/*** 创建一个测试的UserDetail* @return*/@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();User user = new User("admin", "123456", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))) ;inMemoryUserDetailsManager.createUser(user);return inMemoryUserDetailsManager;}/*** 注入密码的验证管理器* @return*/@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}
}
2.5 获取token测试
第一步:
第二步:
三、JWT接入
3.1 Token共享问题
我的token 目前存储在内存里面:
也就是说,当我们仅仅只有一台authorization-server 时,没有任何问题,但是当我们使用多台authorization-server时,由于内存数据无法共享,故用户登录的数据仅仅保存在一台服务器里面,这就会导致某台授权服务器会误判“是否用户登录”这个问题。
3.2 使用Redis 共享Token
将之前数据存储在内存里面的问题解决掉,现在直接把token 存储在内存里面:
3.2.1 添加依赖
<!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.3.2.RELEASE</version></dependency>
3.2.2 添加配置文件
server:port: 9999
spring:application:name: authorization-servercloud:nacos:discovery:server-addr: localhost:8848redis:port: 6379host: 127.0.0.1password: 123456database: 0 #指定数据库
注意:redis-server 要事先在host文件里面配置。
3.2.3 使用RedisTokenStore
修改我们之前的AuthorizationServerConfig
配置类:
@EnableAuthorizationServer // 开启授权服务器的功能
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate PasswordEncoder passwordEncoder ;@Autowiredprivate AuthenticationManager authenticationManager ;@Autowiredprivate UserDetailsService userDetailsService ;//注入redis@Autowiredprivate RedisConnectionFactory redisConnectionFactory ;/*** 添加第三方的客户端*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("coin-api") // 第三方客户端的名称.secret(passwordEncoder.encode("coin-secret")) // 第三方客户端的密钥.scopes("all") //第三方客户端的授权范围.accessTokenValiditySeconds(24*3600) // token的有效期.refreshTokenValiditySeconds(24*7*3600);// refresh_token的有效期super.configure(clients);}/*** 配置验证管理器,UserdetailService*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService).tokenStore(redisTokenStore());super.configure(endpoints);}public TokenStore redisTokenStore(){return new RedisTokenStore(redisConnectionFactory) ;}
}
3.2.4 获取Token 测试
重启authorization-server
,获取token测试
3.2.5 观察Redis数据
我们发现,在redis 里面已经保存了用户登录的数据了。
3.3 资源服务器和授权服务的交互
3.3.1 在授权服务器里面准备userinfo的接口
@RestController
public class UserInfoController {/*** 获取该用户的对象* @param principal* @return*/@GetMapping("/user/info")public Principal usrInfo(Principal principal){ // 此处的principal 由OAuth2.0 框架自动注入// 原理就是:利用Context概念,将授权用户放在线程里面,利用ThreadLocal来获取当前的用户对象
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();return principal ;}
}
3.3.2 将该授权服务器变成资源服务器
因为授权服务器里面提供了userinfo 该资源,所以我们也将它认为是授权服务器。
添加配置类就可以了。
@EnableResourceServer
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {}
3.3.2使用token 换取user对象
第一步:获取一个token
第二步:使用Token 换用户对象
我们可以看见,user 对象已经被获取到了。
3.3.3 存在的问题
我们在理解资源服务器和授权服务的交互后会发现,授权服务器有巨大的压力:当用户访问每一个受保护的资源时(无论该资源散落在那个微服务里面),资源服务器都要和授权服务器交互一次,这样,授权服务器的压力将非常的大。我们必须找解决方案。
- 方案一:使用负载均衡的概念,多部署几台授权服务器
- 方案二:让资源服务器不再访问授权服务器
3.4 使用Jwt来做token的存储
上面的方案里面,我们提到了让资源服务器不再访问授权服务器,那会存在什么问题呢?
资源服务器访问授权服务的本质在于2点:
- 第一点:资源服务器无法验证token的正确性,因为它没有存储token
- 第二点:资源服务要通过授权服务器来换取用户(token 换 user)。
我们来推演:资源服务器当前只能得到用户给他的token,我们能做的改造有限:
- 第一步:若我们将用户的基本信息存储在token 里面呢?
- 第二步:定义一种加密规则,让资源服务器也能去判断该token的正确性。
这样,我们的JWT就上场了。看看JWT的定义:
3.4.1 生成私钥和公钥
生成私钥:
keytool -genkeypair -alias coinexchange -keyalg RSA -keypass coinexchange -keystore coinexchange.jks -validity 365 -storepass coinexchange
具体命令和参数:
Keytool 是一个java提供的证书管理工具
现在,刚刚运行命令的目录上已经有一个jks 文件了。
该文件里面保存的就是私钥信息。
解析公钥:
要去下载openssl
openssl下载地址
我下的这个
一直点下一步然后配置环系统境变量。
就是你自己openssl
安装的目录
安装之后就可以解析公钥了
keytool -list -rfc --keystore coinexchange.jks | openssl x509 -inform pem -pubkey
要输入你刚刚创建时输入的密码
将解析出来的公钥放在一个文件的文件里面:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArbzkbzTuolRUWzdGUfj/
cc5BHuQeTXUJuvfHtEFQf5yl2ZZ9Q6banG5Bb9ph9/v5C1BjeoJYtzJoiMfHUOFs
BLIYwseII4pt38OQJ4SVu1okOEPv+mgbNxHdyfX0etROCKKFBQrvV+N21IO/meRJ
YlXylmWt4/wh78G3jgXFsnCr/VAUqRGxDPA+r3zAXNAFXAiJFEOzvBq+8+QLQ/hv
lzN2asfr0M4b/N1mgO6N3atpat3updLD0zzOZ0P8vDhJzNCgPTQe5urxoSg8BH1M
BIH8Qx3Mfwq5Lf+SZjCWKzRZpw047MH3ReEER4E0s1F0mmS5MEMWsjrlzzTzY+T7
ewIDAQAB
-----END PUBLIC KEY-----
保存好,以后备用。
3.4.2 修改配置文件,不要redis了
server:port: 9999
spring:application:name: authorization-servercloud:nacos:discovery:server-addr: nacos-server:8848
pom文件redis的依赖也不要了。
删了注释了也行
3.4.3 将私钥文件复制到resource下
3.4.4 修改配置类载入私钥文件
@EnableAuthorizationServer // 开启授权服务器的功能
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate PasswordEncoder passwordEncoder ;@Autowiredprivate AuthenticationManager authenticationManager ;@Autowiredprivate UserDetailsService userDetailsService ;// @Autowired
// private RedisConnectionFactory redisConnectionFactory ;/*** 添加第三方的客户端*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("coin-api") // 第三方客户端的名称.secret(passwordEncoder.encode("coin-secret")) // 第三方客户端的密钥.scopes("all") //第三方客户端的授权范围.accessTokenValiditySeconds(24*3600) // token的有效期.refreshTokenValiditySeconds(24*7*3600);// refresh_token的有效期super.configure(clients);}/*** 配置验证管理器,UserdetailService*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService)// .tokenStore(redisTokenStore()); 不用redis了.tokenStore(jwtTokenStore()) //设置token 存储在哪里.tokenEnhancer(jwtAccessTokenConverter()) ;super.configure(endpoints);}//不用了redis,用jwt
// public TokenStore redisTokenStore(){// return new RedisTokenStore(redisConnectionFactory) ;
// }/*** jwtTokenStore* @return*/public TokenStore jwtTokenStore(){JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());return jwtTokenStore ;}public JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter() ;// 读取classpath 下面的密钥文件ClassPathResource classPathResource = new ClassPathResource("coinexchange.jks");// 获取KeyStoreFactoryKeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,"coinexchange".toCharArray()) ;// 给JwtAccessTokenConverter 设置一个密钥对tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("coinexchange","coinexchange".toCharArray()));return tokenConverter ;}
}
3.4.5 获取token 测试
3.4.6 使用jwt.io 校验token
地址:https://jwt.io/
四、JWT的登出问题
Jwt 使用起来不难,而且让我们将“无状态”的概念更贴切的展示出来了,但是实践就真的这么完美吗?不是,因为jwt的登出问题。
何为登出:就是用户自己点击登出后,或用户的角色/权限改变后,该token 仍然是有效的。你可以选择在前端清除该token,但是,如果用户是有技术背景的黑客呢?之前的token他保存一边,在没有过期(时间过期)时,他仍然可以使用该token。
解决方案:
就是删除该用户存储在redis里面登录的token数据
4.1 在网关里面判断该token是否存在
老师的源码:
@Component
public class JwtCheckFilter implements GlobalFilter, Ordered {@Autowiredprivate StringRedisTemplate redisTemplate ;@Value("${no.require.urls:/admin/login,/user/gt/register,/user/login,/user/users/register,/user/sms/sendTo,/user/users/setPassword}")private Set<String> noRequireTokenUris ;/*** 过滤器拦截到用户的请求后做啥* @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1 : 该接口是否需要token 才能访问if(!isRequireToken(exchange)){return chain.filter(exchange) ;// 不需要token ,直接放行 }// 2: 取出用户的tokenString token = getUserToken(exchange) ;// 3 判断用户的token 是否有效if(StringUtils.isEmpty(token)){return buildeNoAuthorizationResult(exchange) ;}Boolean hasKey = redisTemplate.hasKey(token);if(hasKey!=null && hasKey){return chain.filter(exchange) ;// token有效 ,直接放行 }return buildeNoAuthorizationResult(exchange) ;}/*** 给用户响应一个没有token的错误* @param exchange* @return*/private Mono<Void> buildeNoAuthorizationResult(ServerWebExchange exchange) {ServerHttpResponse response = exchange.getResponse();response.getHeaders().set("Content-Type","application/json");response.setStatusCode(HttpStatus.UNAUTHORIZED) ;JSONObject jsonObject = new JSONObject();jsonObject.put("error","NoAuthorization") ;jsonObject.put("errorMsg","Token is Null or Error") ;DataBuffer wrap = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());return response.writeWith(Flux.just(wrap)) ;}/*** 从 请求头里面获取用户的token* @param exchange* @return*/private String getUserToken(ServerWebExchange exchange) {String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);return token ==null ? null : token.replace("bearer ","") ;}/*** 判断该 接口是否需要token* @param exchange* @return*/private boolean isRequireToken(ServerWebExchange exchange) {String path = exchange.getRequest().getURI().getPath();if(noRequireTokenUris.contains(path)){return false ; // 不需要token}return Boolean.TRUE ;}/*** 拦截器的顺序* @return*/@Overridepublic int getOrder() {return 0;}
}
自己写的:
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {@Value("${no.token.access.urls:/admin/login,/admin/validate/code}")private Set<String> noTokenAccessUrls;/*** 实现判断用户是否携带token ,或token 错误的功能** @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//不需要token就能访问if(allowNoTokenAccess(exchange)){return chain.filter(exchange);}// 获取用户的tokenString token=getToken(exchange);//token为空if (StringUtils.isEmpty(token)) { // token 为 Emptyreturn buildUNAuthorizedResult(exchange);}return chain.filter(exchange);}private Mono<Void> buildUNAuthorizedResult(ServerWebExchange exchange) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.UNAUTHORIZED);//未认证response.getHeaders().set("Content-Type","application/json;charset=UTF-8");HashMap<String, String> map = new HashMap<>();map.put("error","unauthorized");map.put("error_description", "invalid_token");DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));return response.writeWith(Flux.just(dataBuffer));}/*** 获取用户token* @param exchange* @return*/private String getToken(ServerWebExchange exchange) {ServerHttpRequest request = exchange.getRequest();HttpHeaders headers = request.getHeaders();String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);if(Objects.isNull(authorization)||authorization.trim().isEmpty()){return null;}return authorization.replace("bearer","");}private boolean allowNoTokenAccess(ServerWebExchange exchange){String path = exchange.getRequest().getURI().getPath();if(noTokenAccessUrls.contains(path)){//放行登录、验证接口return true;}return false;}@Overridepublic int getOrder() {return 0;}
}
4.2 模拟访问
访问 /admin/login 这样的不需要token的资源时:
访问启动的资源时:
五、授权服务器的接入
之前我们仅仅在authorizaiton-server 里面添加了一个模拟的用户:
还没有接入到我们的系统的用户数据,本节课我们来接入一下我们的用户数据
5.1 添加登录常量
在该登录常量里面,我们可以定义受支持的登录类型
public class LoginConstant {/*** 管理员登录*/public static final String ADMIN_TYPE = "admin_type" ;/*** 用户/会员登录*/public static final String MEMBER_TYPE = "member_type" ;}
5.2 实现UserDetailService 接口
/*** 登录的实现** @param username* @return* @throws UsernameNotFoundException*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();String loginType = requestAttributes.getRequest().getParameter("login_type");if (StringUtils.isEmpty(loginType)) {throw new AuthenticationServiceException("请添加login_type参数");}UserDetails userDetails = null;switch (loginType) {case LoginConstant.ADMIN_TYPE: // 管理员登录userDetails = loadAdminUserByUsername(username);break;case LoginConstant.MEMBER_TYPE: // 会员登录userDetails = loadMemberUserByUsername(username);break;default:throw new AuthenticationServiceException("暂不支持的登录方式" + loginType);}return userDetails;
}
5.3 管理员用户的登录
5.3.1 RBAC模型
RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
sys_user表
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',`username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账号',`password` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',`fullname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',`mobile` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态 0-无效; 1-有效;',`create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',`modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',`created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',`last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1018715142409592835 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '平台用户' ROW_FORMAT = Dynamic;
角色表sys_role
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',`code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代码',`description` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',`create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',`modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态0:禁用 1:启用',`created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',`last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1017767747970568195 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色' ROW_FORMAT = Dynamic;
用户角色关联表sys_user_role
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',`role_id` bigint(18) NULL DEFAULT NULL COMMENT '角色ID',`user_id` bigint(18) NULL DEFAULT NULL COMMENT '用户ID',`create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',`modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',`created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',`last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1022060671264763907 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色配置' ROW_FORMAT = Dynamic;
权限表sys_privilege
-- ----------------------------
-- Table structure for sys_privilege
-- ----------------------------
DROP TABLE IF EXISTS `sys_privilege`;
CREATE TABLE `sys_privilege` (`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',`menu_id` bigint(18) NULL DEFAULT NULL COMMENT '所属菜单Id',`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '功能点名称',`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '功能描述',`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,`create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',`modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',`created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',`last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',PRIMARY KEY (`id`) USING BTREE,INDEX `unq_name`(`name`(191)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1010101010101010193 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '权限配置' ROW_FORMAT = Dynamic;
角色权限关联表sys_role_privilege
-- ----------------------------
-- Table structure for sys_role_privilege
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_privilege`;
CREATE TABLE `sys_role_privilege` (`id` bigint(18) NOT NULL AUTO_INCREMENT,`role_id` bigint(18) NOT NULL,`privilege_id` bigint(18) NOT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1021574920613801987 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色权限配置' ROW_FORMAT = Dynamic;
user表
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '自增id',`type` tinyint(4) NULL DEFAULT 1 COMMENT '用户类型:1-普通用户;2-代理人',`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',`country_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国际电话区号',`mobile` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',`paypassword` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易密码',`paypass_setting` tinyint(1) NULL DEFAULT 0 COMMENT '交易密码设置状态',`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',`real_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名',`id_card_type` tinyint(1) NULL DEFAULT NULL COMMENT '证件类型:1,身份证;2,军官证;3,护照;4,台湾居民通行证;5,港澳居民通行证;9,其他;',`auth_status` tinyint(4) NULL DEFAULT 0 COMMENT '认证状态:0-未认证;1-初级实名认证;2-高级实名认证',`ga_secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Google令牌秘钥',`ga_status` tinyint(1) NULL DEFAULT 0 COMMENT 'Google认证开启状态,0,未启用,1启用',`id_card` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '身份证号',`level` int(11) NULL DEFAULT NULL COMMENT '代理商级别',`authtime` datetime(0) NULL DEFAULT NULL COMMENT '认证时间',`logins` int(11) NULL DEFAULT 0 COMMENT '登录数',`status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0,禁用;1,启用;',`invite_code` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '邀请码',`invite_relation` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '邀请关系',`direct_inviteid` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '直接邀请人ID',`is_deductible` int(11) NULL DEFAULT 0 COMMENT '0 否 1是 是否开启平台币抵扣手续费',`reviews_status` int(11) NULL DEFAULT 0 COMMENT '审核状态,1通过,2拒绝,0,待审核',`agent_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '代理商拒绝原因',`access_key_id` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'API的KEY',`access_key_secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'API的密钥',`refe_auth_id` bigint(30) NULL DEFAULT NULL COMMENT '引用认证状态id',`last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',`created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',PRIMARY KEY (`id`) USING BTREE,INDEX `status`(`status`) USING BTREE,INDEX `idx_addtime`(`created`) USING BTREE,INDEX `username`(`username`(191)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1024859055654637571 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;
5.3.2 依赖导入
我们选择简单的jdbcTemplate 来做权限的查询操作
<!--连接数据库查用户信息和权限--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><version>2.3.2.RELEASE</version></dependency><!--数据库驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.25</version></dependency>
5.3.3 配置数据源
server:port: 9999
spring:application:name: authorization-servercloud:nacos:discovery:server-addr: localhost:8848datasource:url: jdbc:mysql://127.0.0.1:3306/coin-exchange?useSSL=false&serverTimezone=GMT%2B8driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456
5.3.4 准备SQL语句
以下sql 语句都位于LoginConstant 常量里面
一、用于登录
使用用户名查询用户的SQL:
public static final String QUERY_ADMIN_SQL ="SELECT `id` ,`username`, `password`, `status` FROM sys_user WHERE username = ? ";
二、查询用户的权限
- 判断用户是否为管理员
public static final String QUERY_ROLE_CODE_SQL ="SELECT `code` FROM sys_role LEFT JOIN sys_user_role ON sys_role.id = sys_user_role.role_id WHERE sys_user_role.user_id= ?";
- 用户为管理员时:(拥有全部的权限)
public static final String QUERY_ALL_PERMISSIONS ="SELECT `name` FROM sys_privilege";
- 普通用户时(通过用户的角色查询用户的权限)
public static final String QUERY_PERMISSION_SQL ="SELECT * FROM sys_privilege LEFT JOIN sys_role_privilege ON sys_role_privilege.privilege_id = sys_privilege.id LEFT JOIN sys_user_role ON sys_role_privilege.role_id = sys_user_role.role_id WHERE sys_user_role.user_id = ?";
5.3.5 代码实现
/*** 登录的实现** @param username* @return* @throws UsernameNotFoundException*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();String loginType = requestAttributes.getRequest().getParameter("login_type");if (StringUtils.isEmpty(loginType)) {throw new AuthenticationServiceException("请添加login_type参数");}UserDetails userDetails = null;try {switch (loginType) {case LoginConstant.ADMIN_TYPE: // 管理员登录userDetails = loadAdminUserByUsername(username);break;case LoginConstant.MEMBER_TYPE: // 会员登录userDetails = loadMemberUserByUsername(username);break;default:throw new AuthenticationServiceException("暂不支持的登录方式" + loginType);}} catch (IncorrectResultSizeDataAccessException e) {throw new UsernameNotFoundException("会员:" + username + "不存在");}return userDetails;
}/*** 对接管理员的登录** @param username* @return*/
private UserDetails loadAdminUserByUsername(String username) {return jdbcTemplate.queryForObject(QUERY_ADMIN_SQL, new RowMapper<User>() {@Overridepublic User mapRow(ResultSet rs, int rowNum) throws SQLException {if (rs.wasNull()) {throw new UsernameNotFoundException("用户:" + username + "不存在");}Long id = rs.getLong("id");String password = rs.getString("password");int status = rs.getInt("status");User user = new User(String.valueOf(id), // 使用用户的id 代替用户的名称,这样会使得后面的很多情况得以处理password,status == 1,true,true,true,getUserPermissions(id));return user;}}, username);
}/*** 通过用户的id 获取用户的权限** @param id* @return*/
private Set<SimpleGrantedAuthority> getUserPermissions(Long id) {// 查询用户是否为管理员String code = jdbcTemplate.queryForObject(QUERY_ROLE_CODE_SQL, String.class, id);List<String> permissions = null;if (ADMIN_CODE.equals(code)) { // 管理员permissions = jdbcTemplate.queryForList(QUERY_ALL_PERMISSIONS, String.class);} else {permissions = jdbcTemplate.queryForList(QUERY_PERMISSION_SQL, String.class, id);}if (permissions == null || permissions.isEmpty()) {return Collections.EMPTY_SET;}return permissions.stream().distinct() // 去重.map(perm -> new SimpleGrantedAuthority(perm) // perm - >security可以识别的权限).collect(Collectors.toSet());
}
5.3.6 测试效果
注意,先将数据库里面sys_user
表里面,admin用户的密码修改为admin
这样测试起来简单一点。
我们发现,已经获取到了Token,看看token 里面都藏了什么:
登录已经完成了
5.3.7 密码加密器
修改:WebSecurityConfig:将之前的PasswordEncoder 修改为以下的代码:
/*** 注入密码的验证管理器* @return*/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
我们的密码加密器将会影响2 个地方:
- 第三方客户端
2. 用户登录时的密码匹配
5.3.8 测试密码匹配器的效果
先获取测试数据:
public static void main(String[] args) {System.out.println( new BCryptPasswordEncoder().encode("123456"));}
先将数据库里面sys_user表里面,admin用户的密码修改为刚刚输出的:
$2a$10$sYSgrL0P4l/1UTlK/slXkuypXRGc.D.iV7uzhE3545a8yKdEw8XaG
获取Token:
还是没有任何的错误。
5.4 会员登录的接入
会员没有复杂的RBAC模型处理,我们仅仅做简单的登录就可以了。
5.4.1 准备SQL 语句
public static final String QUERY_MEMBER_SQL ="SELECT `id`,`password`, `status` FROM `user` WHERE mobile = ? or email = ? ";
5.4.2 代码实现
/*** 会员登录* @param name* @return*/private UserDetails loadMemberUserByUsername(String name) {return jdbcTemplate.queryForObject(LoginConstant.QUERY_MEMBER_SQL,(rs, rowNum)->{if(rs.wasNull()){throw new UsernameNotFoundException("会员:" + name + "不存在");}long id = rs.getLong("id"); // 获取用户的idString password = rs.getString("password");int status = rs.getInt("status");return new User(String.valueOf(id),password,status == 1 ,true ,true ,true,Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));},name,name);}
5.4.3 测试登录
六、refresh_token和过期时间
我们可以可以使用refresh_token 来为过期的token 获取一个新的token数据
6.1 添加验证方式
6.2 获取信息的token 测试
重启后重新后期:
大家可以看见,我们获取到的数据新增了refresh_token的一项,我们来看看它里面包含那些信息:
基本和之前时没有区别的。
6.3 使用Refresh_token获取新的token
先看错误:
原因在于:我们把jwt 里面的username 换成了现在的 用户 id ,导致的。
现在,我们需要一个纠正的过程:
Refresh_token的标识:(loginConstant)
/*** token的刷新*/
public static final String REFRESH_TOKEN = "REFRESH_TOKEN" ;
@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();String loginType = requestAttributes.getRequest().getParameter("login_type");if (StringUtils.isEmpty(loginType)){throw new AuthenticationServiceException("请添加login_type参数");}//刷新令牌String grantType = requestAttributes.getRequest().getParameter("grant_type");UserDetails userDetails=null;try {if (LoginConstant.REFRESH_TOKEN.equals(grantType.toUpperCase())) {name = adjustUsername(name, loginType); // 为refresh_token 时,需要将id->username}switch (loginType){case LoginConstant.ADMIN_TYPE: //管理员登录userDetails=loadAdminUserByUsername(name);break;case LoginConstant.MEMBER_TYPE://会员登录userDetails = loadMemberUserByUsername(name);break;default:throw new AuthenticationServiceException("暂不支持的登录方式" + loginType);}} catch (AuthenticationServiceException e) {throw new UsernameNotFoundException("会员:" + name + "不存在");}return userDetails;}
纠正的实现:
添加SQL语句:
/*** 使用用户的id 查询用户名称*/
public static final String QUERY_ADMIN_USER_WITH_ID = "SELECT `username` FROM sys_user where id = ?" ;/*** 使用用户的id 查询用户名称*/
public static final String QUERY_MEMBER_USER_WITH_ID = "SELECT `mobile` FROM user where id = ?" ;
实现纠正:
/*** 纠正在refresh 场景下的登录问题* @param username* @param loginType* @return*/private String adjustUsername(String username, String loginType) {if(LoginConstant.ADMIN_TYPE.equals(loginType)){//管理员return jdbcTemplate.queryForObject(LoginConstant.QUERY_ADMIN_USER_WITH_ID,String.class,username);}if(LoginConstant.MEMBER_TYPE.equals(loginType)){return jdbcTemplate.queryForObject(LoginConstant.QUERY_MEMBER_USER_WITH_ID,String.class,username);}return username;}
再测试一下:
参数:
grant_type :refresh_token
login_type:admin_type
refresh_token:刚刚测试得到的refresh_token
6.4 token 过期时间的设置
Token的有效期为一周,
Refresh_token的有效期为一个月。
七、Token传递和获取
7.1 受保护资源之前Token的传递
Case1:
Case2:
在第一种Case 里面,我们可以从本次请求的上下文里面获取用户的token ,进行一个Token的传递。
在第二种Case 里面,我们没有一个用户请求的上下文,因此我们需要应用自己去获取一个临时的token。
这2种请求的源码实现,OAuth2.0 已经帮我们写好了,在:
获取的方式非常的简单:使用client_credentials 授权方式来进行的。
7.2 在authorization-server 里面添加客户端授权的方式
/*** 配置第三方客户端*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("coin-api")//客户端id.secret(passwordEncoder.encode("coin-secret"))//客户端密码,要加密,不然一直要求登录,获取不到临牌,一定不要泄露.scopes("all")//授权范围标识,哪部分资源可访问(all是标识,不是代表所有).authorizedGrantTypes("password","refresh_token") //refresh_token配置这个才能刷新令牌 grant_type对应 password.autoApprove(false)//false 跳转到授权页面手动点击,true不用手动点击,直接响应授权//.redirectUris()//客户端回调地址.accessTokenValiditySeconds(1*60*60)//一个小时.refreshTokenValiditySeconds(60*60*2)//临牌刷新实际.and().withClient("inside-app").secret(passwordEncoder.encode("inside-secret")).scopes("all").authorizedGrantTypes("client_credentials").accessTokenValiditySeconds(7 * 24 *3600) ;super.configure(clients);}
7.3 获取token 测试
成功拿到了token!
搭建授权服务器oauth2相关推荐
- 【转】idea激活搭建授权服务器
1.下载软件:磁力链接: magnet:?xt=urn:btih:2289E4F8CEB346AC44E54C8C0DA706CC537301AA 复制磁力链接地址 magnet:?xt=urn:b ...
- SpringSecurity(二十)---OAuth2:实现资源服务器(上)资源服务器搭建以及直接调用授权服务器模式
一. 前言 本章将讨论如何使用Spring Security实现一个资源服务器,资源服务器是管理用户资源的组件.另外,学习本章有个前提,需要先把前面搭建授权服务器的相关文章先给阅读,否则可能后面出现的 ...
- Spring Security 自定义授权服务器实践
相关文章: OAuth2的定义和运行流程 Spring Security OAuth实现Gitee快捷登录 Spring Security OAuth实现GitHub快捷登录 Spring Secur ...
- 搭建认证服务器 - Spring Security Oauth2.0 集成 Jwt 之 【授权码认证流程】 总结
在搭建介绍流程之前,确保您已经搭建了一个 Eureka 注册中心,因为没有注册中心的话会报错(也有可能我搭建的认证服务器是我项目的一个子模块的原因):Request execution error. ...
- 使用Owin中间件搭建OAuth2.0认证授权服务器
前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OA ...
- php对接AliGenie天猫精灵服务器控制智能硬件esp8266② 全面认识第三方授权机制 oauth2.0 协议,如何在 php 上搭建 oauth2.0服务端!(附带demo)
本系列博客学习由非官方人员 半颗心脏 潜心所力所写,仅仅做个人技术交流分享,不做任何商业用途.如有不对之处,请留言,本人及时更改. 1. php对接AliGenie天猫精灵服务器控制智能硬件esp82 ...
- 正在向icntv服务器认证授权信息,Spring-Security-OAuth2服务器之搭建认证授权服务器[一]...
结构基础 基础框架:Spring Boot + Spring-Security-OAuth2 存储介质:Mysql + Redis 持久化方式:Spring-data-jpa 测试工具:Postman ...
- 使用Spring Security Oauth2 和 JWT保护微服务--Uaa授权服务器的编写
学习自深入理解微服务 采用Spring Security OAuth2 和 JWT的方式,Uaa服务只需要验证一次,返回JWT.返回的JWT包含了用户的所有信息,包括权限信息 从三个方面讲解: JWT ...
- uaa 授权_使用UAA OAuth2授权服务器–客户端和资源
uaa 授权 在上一篇文章中,我介绍了如何使用Cloud Foundry UAA项目启动OAuth2授权服务器,以及如何使用OAuth2授权代码流程中涉及的一些参与者来填充它. 我在Digital O ...
最新文章
- 使用Auto TensorCore CodeGen优化Matmul
- 6.3 OpenSSH
- 如何给一个二维数组动态分配内存
- Charles抓包https(测试app的双向认证)
- 消息系统kafka原理解析
- java枚举使用示例
- PCL: 根据几何规则的曲面剖分-贪婪法表面重建三角网格
- 视图的数据存放在哪里_分布式 | DBLE 是如何实现视图的?
- CCNA初认识——OSPF(开放式最短路径优先协议)配置命令
- Cocos2dx 之 PhysicsHandler 简单实现
- 欧姆龙plc的IO存储器区详解
- MES系统软件如何在数控加工车间应用?
- Flutter 修改App的名称和图标
- java uuid to long,生成long类型的UUID
- redis命令之哈希表类型hget命令用法详情(不支持多field)
- 小米蓝牙耳机airdots青春版双耳模式
- 练习电脑键盘打字最好的网站
- Qt制作的一款即时通讯软件
- oracle append parallel,大量数据快速插入方法探究[nologging+parallel+append]
- ubuntu开机密码破解