一、前言
也许,我应该延续上一篇"(一)spring security:能做什么?"接着写,比如:如何实现RBAC权限动态控制、后端"验证码"生成与校验、JWT令牌如何在前后端分离项目中应用、采用"委派密码编码器",兼容老系统密码等,不能原因是,没时间写样例程序(每天好忙o(╯□╰)o),写好的代码又是公司产品源码(担心牵扯事情),以后有机会再补上吧。
本次处于开发阶段的demo,可以展示了,^_^。
在阅读这篇文章时,原理和流程,我就不再写了(不一定有别人写的好o(* ̄︶ ̄*)o)。最好先阅读以下参考文献,详细阅读 OAuth 2.0相关文章;这样,你去看别人的博客时,才不会手忙脚乱、不知所措。而且,你会领悟 spring OAuth 2是如何配置、如何实现、授权原理是什么,锻炼自己的思考能力;才能已"架构师"的思想,在企业的应用环境中熟练应用与掌握它。
下一篇文章,我将已截图和UML图的形式,展示 spring OAuth 2设计的精华所在,堪称艺术品!
本文所采用的模式是最复杂的 authorization code。演示系统包括 auth_server(授权服务器&资源服务器)、client_demo(客户端)。
authorization code (授权码)是授权服务器用来获取并作为客户端和资源所有者之间的中介。代替直接向资源所有者请求授权,客户端定向资源所有者到一个授权服务器,授权服务器反过来指导资源所有者将授权码返回给客户端。在将授权码返回给客户端之前,授权服务器对资源所有者进行身份验证并获得授权。因为资源所有者只对授权服务器进行身份验证,所以资源所有者的凭据永远不会与客户机共享。
先看一张经常见,但又不熟悉的流程图。
它的步骤如下:
- (A)用户访问客户端,后者将前者导向认证服务器
- (B)用户选择是否给予客户端授权
- (C)假设用户给予授权,认证服务器将用户导向客户端事先指定的“重定向 URI”,同时附上一个授权码
- (D)客户端收到授权码,附上早先的“重定向 URI”向认证服务器申请令牌,这一步是在客户端的后台服务器上完成的,对用户不可见
- (E)认证服务器核对了授权码和重定向URI,确认无误后向客户端发送令牌和更新令牌
上述步骤中所需要的参数:
- A步骤中,客户端申请认证的 URI,包含以下参数:
- response_type:授权类型,必选,此处固定值“code”
- client_id:客户端的ID,必选
- client_secret:客户端的密码,可选
- redirect_uri:重定向URI,可选
- scope:申请的权限范围,可选
- state:客户端当前的状态,可以指定任意值,认证服务器会原封不动的返回这个值 (我访问受保护的资源时,框架自动会生成,并附带上)
二、参考文献
(这篇,帮我理解了整个授权流程,实在不错!)
Spring Security Oauth2 单点登录案例实现和执行流程剖析
https://www.cnblogs.com/xifengxiaoma/p/10043173.html
OAuth 2开发人员指南
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
OAuth2 Boot
https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/html5/
OAuth 2.0
https://www.cnblogs.com/cjsblog/p/9174797.html
Spring Security OAuth 2.0
https://www.cnblogs.com/cjsblog/p/9184173.html
OAuth 2.0 授权码请求
https://www.cnblogs.com/cjsblog/p/9230990.html
Spring Boot OAuth 2.0 客户端
https://www.cnblogs.com/cjsblog/p/9241217.html
OAuth2实现单点登录SSO
https://www.cnblogs.com/cjsblog/p/10548022.html
Spring Security OAuth2 SSO
https://www.cnblogs.com/cjsblog/p/9296361.html
JWT存储token
https://blog.csdn.net/qq_28114159/article/details/106549613?utm_source=app
三、授权服务器配置
1.数据库(沿用上一位博客主的(*^▽^*),其实,在官网也能找到)
--客户端表
CREATE TABLE `oauth_client_details` (
`client_id` VARCHAR(256) CHARACTER SET utf8 NOT NULL,
`resource_ids` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
`client_secret` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
`scope` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
`authorized_grant_types` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
`web_server_redirect_uri` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
`authorities` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
`access_token_validity` INT(11) DEFAULT NULL,
`refresh_token_validity` INT(11) DEFAULT NULL,
`additional_information` VARCHAR(4096) CHARACTER SET utf8 DEFAULT NULL,
`autoapprove` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('OrderManagement', NULL, '$2a$10$8yVwRGY6zB8wv5o0kRgD0ep/HVcvtSZUZsYu/586Egxc1hv3cI9Q6', 'all', 'authorization_code,refresh_token', 'http://localhost:8083/orderSystem/login', NULL, 7200, NULL, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('UserManagement', NULL, '$2a$10$ZRmPFVgE6o2aoaK6hv49pOt5BZIKBDLywCaFkuAs6zYmRkpKHgyuO', 'all', 'authorization_code,refresh_token', 'http://localhost:8082/memberSystem/login', NULL, 7200, NULL, NULL, 'true');
COMMIT;
2.maven
这里没有版本号,是因为在顶级 pom.xml里已经定义;请自行添加吧。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
......
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
3.application-dev.properties
server.port=8080
#系统统一请求前缀,进行uri ant 模式管理
#server.servlet.context-path=/
......
############################## jwt秘钥 ##############################
jwt.signing-key=pp123456
############################## redis缓存 ##############################
spring.session.store-type=redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
......
4.配置"资源服务器"
@SpringBootApplication(scanBasePackages = {"com.xxx.spring.boot"})
public class PrototypeApplication {
public static void main(String[] args) {
SpringApplication.run(PrototypeApplication.class, args);
}
}
@EnableResourceServer
此类,感觉本次并没有发挥应有的作用,后续研究下,在单点登录中,如何控制资源权限。
/**
* @desc: 资源服务器配置
* @author: yanfei
* @date: 2020/10/28
*/
@Configuration
@EnableResourceServer
public class SsoResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${jwt.signing-key}")
private String SIGNING_KEY;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new OAuthRequestedMatcher()).authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
private static class OAuthRequestedMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
// Determine if the client request contained an OAuth Authorization
boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
boolean haveAccessToken = request.getParameter("access_token") != null;
return haveOauth2Token || haveAccessToken;
}
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
}
5.@EnableAuthorizationServer
ClientDetailsServiceConfigurer 可用使用 client details service的两种实现中的任意一种:in-memory 或者 JDBC 。
我使用 JDBC,它会在授权过程中,时不时就查询 oauth_client_details表。
/**
* @desc: 授权服务器配置
* @author: yanfei
* @date: 2020/10/26
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Value("${jwt.signing-key}")
private String SIGNING_KEY;
/**
* 配置 AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许客户表单认证
security.allowFormAuthenticationForClients()
//设置oauth_client_details中的密码编码器
.passwordEncoder(new BCryptPasswordEncoder())
.tokenKeyAccess("isAuthenticated()");
}
/**
* 配置 OAuth2的客户端相关信息(数据库中 oauth_client_details)
*
* 客户端授权类型 authorized_grant_types : authorization_code,password,client_credentials,implicit,或refresh_token
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
/**
* 配置 AuthorizationServerEndpointsConfigurer众多相关类,包括配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
* pathMapping() 方法配置端点URL:
* /oauth/authorize:授权端点
* /oauth/token:令牌端点
* /oauth/confirm_access:用户确认授权提交端点
* /oauth/error:授权服务错误信息端点
* /oauth/check_token:用于资源服务访问的令牌解析端点
* /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter())
.tokenStore(jwtTokenStore());
}
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
}
6.@EnableWebSecurity
UserDetailsService 的实现类,我就不再贴出来了。很简单的,自己根据自己的用户表,建立 service实现此接口封装就行了。
这里有些改变,是基于我的系统框架做的配置,所以不再介绍了。后续文章里会找到答案吧。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 表单登录接口
*/
private final String LOGIN_PROCESSING = "/login";
@Bean
public UserDetailsService getUserDetailsService() {
return new BaseAcUserManagerImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//user Details Service验证
auth.userDetailsService(getUserDetailsService()).passwordEncoder(passwordEncoder());
}
/**
* 采用"委派密码编码器",兼容老系统密码
* @return
*/
@Bean
public static PasswordEncoder passwordEncoder() {
//默认 bcrypt
DelegatingPasswordEncoder delegatingPasswordEncoder =
(DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
return delegatingPasswordEncoder;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage(LOGIN_PROCESSING).permitAll()
.and()
//端点:/oauth/authorize应该使用Spring Security保护Authorization端点(或其映射的替代物),以便只有经过身份验证的用户才能访问它
.authorizeRequests()
.anyRequest()
.authenticated()
.and().csrf().disable().cors()
.and()
//因为改变"角色投票器"默认的"ROLE_"前缀,重写 AnonymousAuthenticationToken中包含的角色 “ROLE_ANONYMOUS”
.anonymous().authorities(new String[]{UnifyRoleNoEnum.R_ANONYMOUS.getRoleNo()});
}
/**
* 不走 Spring Security 过滤器链,静态资源放行
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
String staticList = PropKit.use("ant-uri.properties").get("uri.static.list");
web.ignoring().antMatchers(staticList.split(","));
super.configure(web);
}
}
7.登录页和首页
html也是我沿用的他人的,你们可以从上边的博客中寻找到。
/**
* @desc:
* @author: yanfei
* @date: 2020/10/26
*/
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/")
public String index() {
return "index";
}
}
四、客户端配置
1. application.yml
auth-server: http://localhost:8080
server:
port: 8083
servlet:
context-path: /orderSystem
session:
cookie:
name: CLIENT-ORDER-SESSION
security:
oauth2:
client:
client-id: OrderManagement
client-secret: order123
access-token-uri: ${auth-server}/oauth/token
user-authorization-uri: ${auth-server}/oauth/authorize
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key
# key-value:
# key-password:
# user-info-uri,暂时未用到
user-info-uri: ${auth-server}/user
sso:
login-path: /login
2. @EnableOAuth2Sso
【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
https://www.cnblogs.com/trust-freedom/p/12002089.html
/**
* @author yanfei
* @date 2020/10/28
*/
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable()
.logout().logoutSuccessUrl("http://localhost:8080/logout")
.and().antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
3. 受保护的资源
/**
* @author yanfei
* @date 2020/10/28
*/
@Controller
@RequestMapping("/order")
public class OrderController {
@GetMapping("/list")
public String list() {
return "order/list";
}
}
五、数据库 oauth_client_details
OAuth2相关数据表字段的详细说明
https://blog.csdn.net/qq_34997906/article/details/89609297
六、演示:"授权码"获得 JWT令牌,访问受保护的资源
七、下期预告
(三)spring security:OAuth2 SSO "授权码"获得 JWT令牌,访问受保护的资源,源码分析流程