GVKun编程网logo

Spring Security 实现 OAuth2.0 授权服务 - 进阶版(spring security授权服务器)

4

如果您对SpringSecurity实现OAuth2.0授权服务-进阶版和springsecurity授权服务器感兴趣,那么这篇文章一定是您不可错过的。我们将详细讲解SpringSecurity实现O

如果您对Spring Security 实现 OAuth2.0 授权服务 - 进阶版spring security授权服务器感兴趣,那么这篇文章一定是您不可错过的。我们将详细讲解Spring Security 实现 OAuth2.0 授权服务 - 进阶版的各种细节,并对spring security授权服务器进行深入的分析,此外还有关于OAuth2 协议与 Spring Security OAuth2 集成、OAuth2.0协议专区-Springcloud集成springsecurity oauth2实现服务统一认证,应该时最简单的教程了~、Spring Boot HttpSecurity - @PreAuthorize - 如何设置 AuthenticationFilter? 使用 Spring Security 的内置 JWT 支持无需授权服务器即可使用 Spring Security 的内置 JWT 支持自己动手、Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器的实用技巧。

本文目录一览:

Spring Security 实现 OAuth2.0 授权服务 - 进阶版(spring security授权服务器)

Spring Security 实现 OAuth2.0 授权服务 - 进阶版(spring security授权服务器)

 

《Spring Security 实现 OAuth2.0 授权服务 - 基础版》介绍了如何使用 Spring Security 实现 OAuth2.0 授权和资源保护,但是使用的都是 Spring Security 默认的登录页、授权页,client 和 token 信息也是保存在内存中的。

 

本文将介绍如何在 Spring Security OAuth 项目中自定义登录页面、自定义授权页面、数据库配置 client 信息、数据库保存授权码和 token 令牌。

 

一、引入依赖

需要在基础版之上引入 thymeleaf、JDBC、mybatis、mysql 等依赖。

 1 <!-- thymeleaf -->
 2 <dependency>
 3     <groupId>org.springframework.boot</groupId>
 4     <artifactId>spring-boot-starter-thymeleaf</artifactId>
 5 </dependency>
 6 <dependency>
 7     <groupId>org.thymeleaf.extras</groupId>
 8     <artifactId>thymeleaf-extras-springsecurity4</artifactId>
 9 </dependency>
10 
11 <!-- JDBC -->
12 <dependency>
13     <groupId>org.springframework.boot</groupId>
14     <artifactId>spring-boot-starter-jdbc</artifactId>
15 </dependency>
16 <dependency>
17     <groupId>org.apache.commons</groupId>
18     <artifactId>commons-dbcp2</artifactId>
19 </dependency>
20 
21 <!-- Mybatis -->
22 <dependency>
23     <groupId>org.mybatis.spring.boot</groupId>
24     <artifactId>mybatis-spring-boot-starter</artifactId>
25     <version>1.1.1</version>
26 </dependency>
27 
28 <!-- MySQL -->
29 <dependency>
30     <groupId>mysql</groupId>
31     <artifactId>mysql-connector-java</artifactId>
32 </dependency>

 

二、client 和 token 表

 1 -- used in tests that use HSQL
 2 create table oauth_client_details (
 3   client_id VARCHAR(255) PRIMARY KEY,
 4   resource_ids VARCHAR(255),
 5   client_secret VARCHAR(255),
 6   scope VARCHAR(255),
 7   authorized_grant_types VARCHAR(255),
 8   web_server_redirect_uri VARCHAR(255),
 9   authorities VARCHAR(255),
10   access_token_validity INTEGER,
11   refresh_token_validity INTEGER,
12   additional_information TEXT(4096),
13   autoapprove VARCHAR(255)
14 );
15 
16 create table oauth_client_token (
17   token_id VARCHAR(255),
18   token BLOB,
19   authentication_id VARCHAR(255) PRIMARY KEY,
20   user_name VARCHAR(255),
21   client_id VARCHAR(255)
22 );
23 
24 create table oauth_access_token (
25   token_id VARCHAR(255),
26   token BLOB,
27   authentication_id VARCHAR(255) PRIMARY KEY,
28   user_name VARCHAR(255),
29   client_id VARCHAR(255),
30   authentication BLOB,
31   refresh_token VARCHAR(255)
32 );
33 
34 create table oauth_refresh_token (
35   token_id VARCHAR(255),
36   token BLOB,
37   authentication BLOB
38 );
39 
40 create table oauth_code (
41   code VARCHAR(255), authentication BLOB
42 );
43 
44 create table oauth_approvals (
45     userId VARCHAR(255),
46     clientId VARCHAR(255),
47     scope VARCHAR(255),
48     status VARCHAR(10),
49     expiresAt TIMESTAMP,
50     lastModifiedAt TIMESTAMP
51 );
52 
53 
54 -- customized oauth_client_details table
55 create table ClientDetails (
56   appId VARCHAR(255) PRIMARY KEY,
57   resourceIds VARCHAR(255),
58   appSecret VARCHAR(255),
59   scope VARCHAR(255),
60   grantTypes VARCHAR(255),
61   redirectUrl VARCHAR(255),
62   authorities VARCHAR(255),
63   access_token_validity INTEGER,
64   refresh_token_validity INTEGER,
65   additionalInformation VARCHAR(4096),
66   autoApproveScopes VARCHAR(255)
67 );
View Code

 

在 oauth_client_details 表添加数据:

1 INSERT INTO `oauth_client_details` VALUES (''net5ijy'', NULL, ''123456'', ''all,read,write'', ''authorization_code,refresh_token,password'', NULL, ''ROLE_TRUSTED_CLIENT'', 7200, 7200, NULL, NULL);
2 INSERT INTO `oauth_client_details` VALUES (''tencent'', NULL, ''123456'', ''all,read,write'', ''authorization_code,refresh_code'', NULL, ''ROLE_TRUSTED_CLIENT'', 3600, 3600, NULL, NULL);

 

三、用户、角色表

 1 CREATE TABLE `springcloud_user` (
 2 `id`  int(11) NOT NULL AUTO_INCREMENT ,
 3 `username`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
 4 `password`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
 5 `phone`  varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
 6 `email`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
 7 `create_time`  datetime NOT NULL ,
 8 PRIMARY KEY (`id`)
 9 )
10 ENGINE=InnoDB
11 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
12 AUTO_INCREMENT=1;
13 
14 CREATE TABLE `springcloud_role` (
15 `id`  int(11) NOT NULL AUTO_INCREMENT ,
16 `name`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
17 PRIMARY KEY (`id`)
18 )
19 ENGINE=InnoDB
20 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
21 AUTO_INCREMENT=1;
22 
23 CREATE TABLE `springcloud_user_role` (
24 `user_id`  int(11) NOT NULL ,
25 `role_id`  int(11) NOT NULL ,
26 FOREIGN KEY (`role_id`) REFERENCES `springcloud_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
27 FOREIGN KEY (`user_id`) REFERENCES `springcloud_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
28 INDEX `user_id_fk` USING BTREE (`user_id`) ,
29 INDEX `role_id_fk` USING BTREE (`role_id`) 
30 )
31 ENGINE=InnoDB
32 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;
View Code

 

用户表添加数据

1 INSERT INTO `springcloud_user` VALUES (1, ''admin001'', ''$2a$10$sXHKvdufrEfE2900ME40nOSBmeHRRUOF71szu22uaqqL8FIJeJDYW'', ''13622114309'', ''13622114309@189.cn'', ''2019-4-7 09:31:07'');
2 INSERT INTO `springcloud_user` VALUES (2, ''admin002'', ''$2a$10$sXHKvdufrEfE2900ME40nOSBmeHRRUOF71szu22uaqqL8FIJeJDYW'', ''17809837654'', ''17809837654@189.cn'', ''2019-4-7 09:33:00'');

 

角色表添加数据

1 INSERT INTO `springcloud_role` VALUES (1, ''ADMIN'');
2 INSERT INTO `springcloud_role` VALUES (2, ''DBA'');
3 INSERT INTO `springcloud_role` VALUES (3, ''USER'');

 

用户角色关系表添加数据

1 INSERT INTO `springcloud_user_role` VALUES (1, 1);
2 INSERT INTO `springcloud_user_role` VALUES (2, 1);

 

四、实体类和工具类

1、User 实体类

封装授权服务器登录用户信息

 1 public class User implements Serializable {
 2 
 3     private Integer id;
 4     private String username;
 5     private String password;
 6     private String phone;
 7     private String email;
 8     private Set<Role> roles = new HashSet<Role>();
 9     private Date createTime;
10 
11     // getter & setter
12 
13     @Override
14     public int hashCode() {
15         final int prime = 31;
16         int result = 1;
17         result = prime * result + ((id == null) ? 0 : id.hashCode());
18         return result;
19     }
20     @Override
21     public boolean equals(Object obj) {
22         if (this == obj)
23             return true;
24         if (obj == null)
25             return false;
26         if (getClass() != obj.getClass())
27             return false;
28         User other = (User) obj;
29         if (id == null) {
30             if (other.id != null)
31                 return false;
32         } else if (!id.equals(other.id))
33             return false;
34         return true;
35     }
36     @Override
37     public String toString() {
38         return "User [id=" + id + ", username=" + username + ", password="
39                 + password + ", phone=" + phone + ", email=" + email
40                 + ", roles=" + roles + ", createTime=" + createTime + "]";
41     }
42 }
View Code

 

2、Role 实体类

封装角色信息

 1 public class Role implements Serializable {
 2 
 3     private Integer id;
 4     private String name;
 5 
 6     public Role() {
 7         super();
 8     }
 9     public Role(String name) {
10         super();
11         this.name = name;
12     }
13 
14     // getter & setter
15 
16     @Override
17     public String toString() {
18         return "Role [id=" + id + ", name=" + name + "]";
19     }
20 }

 

3、ResponseMessage 工具类

封装接口响应信息

 1 public class ResponseMessage {
 2 
 3     private Integer code;
 4     private String message;
 5 
 6     public ResponseMessage() {
 7         super();
 8     }
 9 
10     public ResponseMessage(Integer code, String message) {
11         super();
12         this.code = code;
13         this.message = message;
14     }
15 
16     // getter & setter
17 
18     public static ResponseMessage success() {
19         return new ResponseMessage(0, "操作成功");
20     }
21 
22     public static ResponseMessage fail() {
23         return new ResponseMessage(99, "操作失败");
24     }
25 }
View Code

 

五、DAO 和 Service 编写

1、数据源配置

在 application.properties 文件配置 datasource

 1 spring.datasource.url=jdbc:mysql://localhost:3306/test
 2 spring.datasource.username=system
 3 spring.datasource.password=123456
 4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 5 
 6 spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
 7 spring.datasource.dbcp2.initial-size=5
 8 spring.datasource.dbcp2.max-active=25
 9 spring.datasource.dbcp2.max-idle=10
10 spring.datasource.dbcp2.min-idle=5
11 spring.datasource.dbcp2.max-wait-millis=10000
12 spring.datasource.dbcp2.validation-query=SELECT 1
13 spring.datasource.dbcp2.connection-properties=characterEncoding=utf8

 

使用 dbcp2 数据源

 

2、mapper.xml

在 src/main/resources 下创建 org.net5ijy.oauth2.mapper 包,创建 user-mapper.xml 配置文件

 1 <mapper namespace="org.net5ijy.oauth2.repository.UserRepository">
 2 
 3     <resultMap type="User" id="UserResultMap">
 4         <result column="id" property="id" jdbcType="INTEGER" javaType="int" />
 5         <result column="username" property="username" jdbcType="VARCHAR"
 6             javaType="string" />
 7         <result column="password" property="password" jdbcType="VARCHAR"
 8             javaType="string" />
 9         <result column="phone" property="phone" jdbcType="VARCHAR"
10             javaType="string" />
11         <result column="email" property="email" jdbcType="VARCHAR"
12             javaType="string" />
13         <result column="create_time" property="createTime" jdbcType="TIMESTAMP"
14             javaType="java.util.Date" />
15         <collection property="roles" select="selectRolesByUserId"
16             column="id"></collection>
17     </resultMap>
18 
19     <!-- 根据用户名查询用户 -->
20     <select id="findByUsername" parameterType="java.lang.String"
21         resultMap="UserResultMap">
22         <![CDATA[
23         select * from springcloud_user where username = #{username}
24         ]]>
25     </select>
26 
27     <!-- 根据user id查询用户拥有的role -->
28     <select id="selectRolesByUserId" parameterType="java.lang.Integer"
29         resultType="Role">
30         <![CDATA[
31         select r.id, r.name from springcloud_user_role ur, springcloud_role r
32         where ur.role_id = r.id and ur.user_id = #{id}
33         ]]>
34     </select>
35 
36 </mapper>
View Code

 

因为我们的例子只使用了 findByUsername 功能,所以只写这个 sql 就可以了

 

3、DAO 接口

1 public interface UserRepository {
2 
3     User findByUsername(String username);
4 }

 

4、UserService

接口

1 public interface UserService {
2 
3     User getUser(String username);
4 }

 

实现类

 1 @Service
 2 public class UserServiceImpl implements UserService {
 3 
 4     static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
 5 
 6     @Autowired
 7     private UserRepository userRepository;
 8 
 9     @Autowired
10     private JdbcTemplate jdbcTemplate;
11 
12     @Override
13     public User getUser(String username) {
14         return userRepository.findByUsername(username);
15     }
16 }

 

5、UserDetailsService 实现类

这个接口的实现类需要在 Security 中配置,Security 会使用这个类根据用户名查询用户信息,然后进行用户名、密码的验证。主要就是实现 loadUserByUsername 方法:

 1 @Service
 2 public class UserDetailsServiceImpl implements UserDetailsService {
 3 
 4     @Autowired
 5     private UserService userService;
 6 
 7     @Override
 8     public UserDetails loadUserByUsername(String username)
 9             throws UsernameNotFoundException {
10 
11         User user = userService.getUser(username);
12         if (user == null || user.getId() < 1) {
13             throw new UsernameNotFoundException("Username not found: "
14                     + username);
15         }
16 
17         return new org.springframework.security.core.userdetails.User(
18                 user.getUsername(), user.getPassword(), true, true, true, true,
19                 getGrantedAuthorities(user));
20     }
21 
22     private Collection<? extends GrantedAuthority> getGrantedAuthorities(
23             User user) {
24         Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
25         for (Role role : user.getRoles()) {
26             authorities
27                     .add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
28         }
29         return authorities;
30     }
31 }
View Code

 

六、自定义登录页面

1、controller 编写

编写 LoginController 类,添加 login 方法

 1 @RestController
 2 public class LoginController {
 3 
 4     @GetMapping("/login")
 5     public ModelAndView login() {
 6         return new ModelAndView("login");
 7     }
 8 
 9     @GetMapping("/login-error")
10     public ModelAndView loginError(HttpServletRequest request, Model model) {
11         model.addAttribute("loginError", true);
12         model.addAttribute("errorMsg", "登陆失败,账号或者密码错误!");
13         return new ModelAndView("login", "userModel", model);
14     }
15 }

 

2、页面代码

页面代码使用到了 thymeleaf、bootstrap、表单验证等,具体的 js、css 引入就不赘述了,只记录最主要的内容:

 1 <div>
 2     <form th:action="@{/login}" method="post">
 3         <div>
 4             <label>&ensp;&ensp;名: </label>
 5             <div>
 6                 <input name="username" />
 7             </div>
 8         </div>
 9         <div>
10             <label>密&#12288;&#12288;码: </label>
11             <div>
12                 <input type="password" name="password" />
13             </div>
14         </div>
15         <div>
16             <div>
17                 <button type="submit"> 登 陆 </button>
18             </div>
19         </div>
20     </form>
21 </div>

 

七、自定义授权页面

1、controller 编写

编写 GrantController 类,添加 getAccessConfirmation 方法

 1 @Controller
 2 @SessionAttributes("authorizationRequest")
 3 public class GrantController {
 4 
 5     @RequestMapping("/oauth/confirm_access")
 6     public ModelAndView getAccessConfirmation(Map<String, Object> model,
 7             HttpServletRequest request) throws Exception {
 8 
 9         AuthorizationRequest authorizationRequest = (AuthorizationRequest) model
10                 .get("authorizationRequest");
11 
12         ModelAndView view = new ModelAndView("base-grant");
13         view.addObject("clientId", authorizationRequest.getClientId());
14 
15         return view;
16     }
17 }

 

此处获取到申请授权的 clientid 用于在页面展示

 

2、页面代码

此处只写最主要的部分

 1 <div>
 2     <div>
 3         <div>OAUTH-BOOT 授权</div>
 4         <div>
 5             <a href="javascript:;">帮助</a>
 6         </div>
 7     </div>
 8     <h3 th:text="${clientId}+'' 请求授权,该应用将获取您的以下信息''"></h3>
 9     <p>昵称,头像和性别</p>
10     授权后表明您已同意 <a href="javascript:;" style="color: #E9686B">OAUTH-BOOT 服务协议</a>
11     <form method="post" action="/oauth/authorize">
12         <input type="hidden" name="user_oauth_approval" value="true" />
13         <input type="hidden" name="scope.all" value="true" />
14         <br />
15         <button class="btn" type="submit">同意/授权</button>
16     </form>
17 </div>

 

八、配置类和 application.properties 配置

1、配置 mybatis

配置 SqlSessionFactoryBean

  • 设置数据源
  • 设置包别名
  • 设置 mapper 映射文件所在的包

 

 1 @Configuration
 2 public class MyBatisConfiguration {
 3 
 4     @Bean
 5     @Autowired
 6     @ConditionalOnMissingBean
 7     public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource)
 8             throws IOException {
 9 
10         SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
11 
12         // 设置数据源
13         sqlSessionFactoryBean.setDataSource(dataSource);
14 
15         // 设置别名包
16         sqlSessionFactoryBean.setTypeAliasesPackage("org.net5ijy.oauth2.bean");
17 
18         // 设置mapper映射文件所在的包
19         PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
20         String packageSearchPath = "classpath*:org/net5ijy/oauth2/mapper/**.xml";
21         sqlSessionFactoryBean
22                 .setMapperLocations(pathMatchingResourcePatternResolver
23                         .getResources(packageSearchPath));
24 
25         return sqlSessionFactoryBean;
26     }
27 }
View Code

 

2、配置 AuthorizationServerConfigurer

  • 配置使用数据库保存 cient 信息
  • 配置使用数据库保存 token 令牌
  • 配置使用数据库保存授权码

 

 1 @Configuration
 2 public class Oauth2AuthorizationServerConfiguration extends
 3         AuthorizationServerConfigurerAdapter {
 4 
 5     @Autowired
 6     private UserDetailsService userDetailsService;
 7 
 8     @Autowired
 9     private AuthenticationManager authenticationManager;
10 
11     @Autowired
12     private DataSource dataSource;
13 
14     @Override
15     public void configure(ClientDetailsServiceConfigurer clients)
16             throws Exception {
17 
18         // 数据库管理client
19         clients.withClientDetails(new JdbcClientDetailsService(dataSource));
20     }
21 
22     @Override
23     public void configure(AuthorizationServerEndpointsConfigurer endpoints)
24             throws Exception {
25 
26         // 用户信息查询服务
27         endpoints.userDetailsService(userDetailsService);
28 
29         // 数据库管理access_token和refresh_token
30         TokenStore tokenStore = new JdbcTokenStore(dataSource);
31 
32         endpoints.tokenStore(tokenStore);
33 
34         ClientDetailsService clientService = new JdbcClientDetailsService(
35                 dataSource);
36 
37         DefaultTokenServices tokenServices = new DefaultTokenServices();
38         tokenServices.setTokenStore(tokenStore);
39         tokenServices.setSupportRefreshToken(true);
40         tokenServices.setClientDetailsService(clientService);
41         // tokenServices.setAccessTokenValiditySeconds(180);
42         // tokenServices.setRefreshTokenValiditySeconds(180);
43 
44         endpoints.tokenServices(tokenServices);
45 
46         endpoints.authenticationManager(authenticationManager);
47 
48         // 数据库管理授权码
49         endpoints.authorizationCodeServices(new JdbcAuthorizationCodeServices(
50                 dataSource));
51         // 数据库管理授权信息
52         ApprovalStore approvalStore = new JdbcApprovalStore(dataSource);
53         endpoints.approvalStore(approvalStore);
54     }
55 }
View Code

 

3、配置 security

  • 配置使用数据库保存登录用户信息
  • 配置自定义登录页面
  • 暂时禁用 CSRF

 

 1 @EnableWebSecurity
 2 public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 3 
 4     @Autowired
 5     private UserDetailsService userDetailsService;
 6 
 7     @Autowired
 8     private PasswordEncoder passwordEncoder;
 9 
10     @Bean
11     public PasswordEncoder passwordEncoder() {
12         return new BCryptPasswordEncoder(); // 使用 BCrypt 加密
13     }
14 
15     public AuthenticationProvider authenticationProvider() {
16         DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
17         authenticationProvider.setUserDetailsService(userDetailsService);
18         authenticationProvider.setPasswordEncoder(passwordEncoder);
19         authenticationProvider.setHideUserNotFoundExceptions(false);
20         return authenticationProvider;
21     }
22 
23     @Override
24     public void configure(WebSecurity web) throws Exception {
25         web.ignoring().antMatchers("/css/**", "/js/**", "/fonts/**",
26                 "/icon/**", "/favicon.ico");
27     }
28 
29     @Override
30     protected void configure(HttpSecurity http) throws Exception {
31 
32         http.requestMatchers()
33                 .antMatchers("/login", "/login-error", "/oauth/authorize",
34                         "/oauth/token").and().authorizeRequests()
35                 .antMatchers("/login").permitAll().anyRequest().authenticated();
36 
37         // 登录页面
38         http.formLogin().loginPage("/login").failureUrl("/login-error");
39 
40         // 禁用CSRF
41         http.csrf().disable();
42     }
43 
44     @Autowired
45     public void configureGlobal(AuthenticationManagerBuilder auth)
46             throws Exception {
47         auth.userDetailsService(userDetailsService);
48         auth.authenticationProvider(authenticationProvider());
49     }
50 
51     public static void main(String[] args) {
52         System.out.println(new BCryptPasswordEncoder().encode("123456"));
53     }
54 }
View Code

 

4、application.properties 文件配置

 1 server.port=7001
 2 
 3 ##### Built-in DataSource #####
 4 spring.datasource.url=jdbc:mysql://localhost:3306/test
 5 spring.datasource.username=system
 6 spring.datasource.password=123456
 7 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 8 
 9 spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
10 spring.datasource.dbcp2.initial-size=5
11 spring.datasource.dbcp2.max-active=25
12 spring.datasource.dbcp2.max-idle=10
13 spring.datasource.dbcp2.min-idle=5
14 spring.datasource.dbcp2.max-wait-millis=10000
15 spring.datasource.dbcp2.validation-query=SELECT 1
16 spring.datasource.dbcp2.connection-properties=characterEncoding=utf8
17 
18 ##### Thymeleaf #####
19 # 编码
20 spring.thymeleaf.encoding=UTF-8
21 # 热部署静态文件
22 spring.thymeleaf.cache=false
23 # 使用HTML5标准
24 spring.thymeleaf.mode=HTML5

 

九、受保护资源

 1 @RestController
 2 @RequestMapping(value = "/order")
 3 public class TestController {
 4 
 5     Logger log = LoggerFactory.getLogger(TestController.class);
 6 
 7     @RequestMapping(value = "/demo")
 8     @ResponseBody
 9     public ResponseMessage getDemo() {
10         Authentication auth = SecurityContextHolder.getContext()
11                 .getAuthentication();
12         log.info(auth.toString());
13         return ResponseMessage.success();
14     }
15 }

 

十、应用启动类

 1 @SpringBootApplication
 2 @EnableAuthorizationServer
 3 @EnableResourceServer
 4 @MapperScan("org.net5ijy.oauth2.repository")
 5 public class Oauth2Application {
 6 
 7     public static void main(String[] args) {
 8 
 9         // args = new String[] { "--debug" };
10 
11         SpringApplication.run(Oauth2Application.class, args);
12     }
13 }

 

十一、测试授权码模式

1、获取 authorization_code 授权码

使用浏览器访问:
http://localhost:7001/oauth/authorize?response_type=code&client_id=net5ijy&redirect_uri=http://localhost:8080&scope=all

 

地址
http://localhost:7001/oauth/authorize

 

参数

response_type

code

client_id

根据实际的 client-id 填写,此处写 net5ijy

redirect_uri

生成 code 后的回调地址,http://localhost:8080

scope

权限范围

 

登录,admin001 和 123456

 

允许授权

 

看到浏览器重定向到了 http://localhost:8080 并携带了 code 参数,这个 code 就是授权服务器生成的授权码

 

2、获取 token 令牌

使用 curl 命令获取 token 令牌

curl --user net5ijy:123456 -X POST -d "grant_type=authorization_code&scope=all&redirect_uri=http%3a%2f%2flocalhost%3a8080&code=ubtvR4" http://localhost:7001/oauth/token

 

地址
http://localhost:7001/oauth/token

 

参数

grant_type

授权码模式,写 authorization_code

scope

权限范围

redirect_uri

回调地址,http://localhost:8080 需要 urlencode

code

就是上一步生成的授权码

 

 

返回值

1 {
2     "access_token": "c5836918-1924-4b0a-be67-043218c6e7e0",
3     "token_type": "bearer",
4     "refresh_token": "7950b7f9-7d60-41da-9a95-bd2c8b29ada1",
5     "expires_in": 7199,
6     "scope": "all"
7 }

 

这样就获取到了 token 令牌,该 token 的访问权限范围是 all 权限,在 2 小时后失效。

 

3、使用 token 访问资源

http://localhost:7001/order/demo?access_token=c5836918-1924-4b0a-be67-043218c6e7e0

 

OAuth2 协议与 Spring Security OAuth2 集成

OAuth2 协议与 Spring Security OAuth2 集成

OAuth 2.0 允许第三方应用程序访问受限的HTTP资源的授权协议,像平常大家使用 Github 、 Google 账号来登陆其他系统时使用的就是 OAuth 2.0 授权框架,下图就是使用 Github 账号登陆 Coding 系统的授权页面图:

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

类似使用 OAuth 2.0 授权的还有很多,本文将介绍 OAuth 2.0 相关的概念如:角色、授权类型等知识,以下是我整理一张 OAuth 2.0 授权的脑头,希望对大家了解 OAuth 2.0 授权协议有帮助。

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

文章将以脑图中的内容展开 OAuth 2.0 协议同时除了 OAuth 2.0 外,还会配合 Spring Security OAuth2 来搭建 OAuth2客户端 ,这也是学习 OAuth 2.0 的目的,直接应用到实际项目中,加深对 OAuth 2.0 和 Spring Security 的理解。

OAuth 2.0 角色

OAuth 2.0 中有四种类型的角色分别为: 资源Owner 、 授权服务 、 客户端 、 资源服务 ,这四个角色负责不同的工作,为了方便理解先给出一张大概的流程图,细节部分后面再分别展开:

OAuth 2.0 大概授权流程

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

资源 Owner

资源 Owner可以理解为一个用户,如之前提到使用 Github 登陆 Coding 中的例子中,用户使用GitHub账号登陆Coding,Coding就需要知道用户在GitHub系统中的的头像、用户名、email等信息,这些账户信息都是属于用户的这样就不难理解 资源 Owner 了。在Coding请求从GitHub中获取想要的用户信息时也是没那容易的,GitHub为了安全起见,至少要通过用户(资源 Owner)的同意才行。

资源服务器

明白 资源 Owner 后,相信你已经知道什么是 资源服务器 ,在这个例子中用户账号的信息都存放在GitHub的服务器中,所以这里的资源服务器就是GitHub服务器。GitHub服务器负责保存、保护用户的资源,任何其他第三方系统想到使用这些信息的系统都需要经过 资源 Owner 授权,同时依照 OAuth 2.0 授权流程进行交互。

客户端

知道 资源 Owner 和 资源服务器 后,OAuth中的客户端角色也相对容易理解了,简单的说 客户端就是想要获取资源的系统 ,如例子中的使用GitHub登陆Coding时,Coding就是OAuth中的客户端。客户端主要负责发起授权请求、获取AccessToken、获取用户资源。

授权服务器

有了 资源 Owner 、 资源服务器 、 客户端 还不能完成OAuth授权的,还需要有 授权服务器 。在OAuth中授权服务器除了负责与用户(资源 Owner)、客房端(Coding)交互外,还要生成AccessToken、验证AccessToken等功能,它是OAuth授权中的非常重要的一环,在例子中授权服务器就是GitHub的服务器。

小结

OAuth中: 资源Owner 、 授权服务 、 客户端 、 资源服务 有四个角色在使用GitHub登陆Coding的例子中分别表示:

  • 资源Owner:GitHub用户
  • 授权服务:GitHub服务器
  • 客户端:Coding系统
  • 资源服务:GitHub服务器

其中授权服务服务器、资源服务器可以单独搭建(鬼知道GitHub怎么搭建的)。在微服务器架构中可单独弄一个授权服务,资源服务服务可以多个如:用户资源、仓库资源等,可根据需求自由分服务。

OAuth2 Endpoint

OAuth2有三个重要的Endpoint其中 授权 Endpoint 、 Token Endpoint 结点在授权服务器中,还有一个可选的 重定向 Endpoint 在客户端中。

授权 Endpoint重定向 Endpoint

授权类型

通过四个OAuth角色,应该对OAuth协议有一个大概的认识,不过可能还是一头雾水不知道OAuth中的角色是如何交互的,没关系继续往下看一下 授权类型 就知道OAuth中的角色是如何完成自己的职责,进一步对OAuth的理解。在OAuth中定义了四种 授权类型 ,分别为:

  • 授权码授权
  • 客房端凭证授权
  • 资源Owner的密码授权
  • 隐式的授权

不同的 授权类型 可以使用在不同的场景中。

授权码授权

这种形式就是我们常见的授权形式(如使用GitHub账号登陆Coding),在整个授权流程中会有 资源Owner 、 授权服务器 、 客户端 三个OAuth角色参与,之所以叫做 授权码授权 是因为在交互流程中授权服务器会给客房端发放一个 code ,随后客房端拿着授权服务器发放的code继续进行授权如:请求授权服务器发放AccessToken。

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

为方便理解再将上图的内容带进真实的场景中,用文字表述一下整个流程:

  • A.1、用户访问Coding登陆页( https://coding.net/login ),点击Github登陆按钮;
  • A.2、Coding服务器将浏览器重定向到Github的授权页( https://github.com/login/oauth/authorize?client_id=a5ce5a6c7e8c39567ca0&scope=user:email&redirect_uri=https://coding.net/api/oauth/github/callback&response_type=code ),同时URL带上 client_id 和 redirect_uri 参数;
  • B.1、用户输入用户名、密码登陆Github;
  • B.2、用户点击 授权按钮 ,同意授权;
  • C.1、Github授权服务器返回 code ;
  • C.2、Github通过将浏览器重定向到 A.2 步骤中传递的 redirect_uri 地址(https://coding.net/api/oauth/github/callback&response_type=code);
  • D、Coding拿到code后,调用Github授权服务器API获取AccessToken,由于这一步是在Coding服务器后台做的浏览器中捕获不到,基本就是使用 code 访问github的access_token节点获取AccessToken;

以上是大致的 授权码授权 流程,大部分是客户端与授权服务器的交互,整个过程中有几个参数说明如下:

  • client_id:在Github中注册的Appid,用于标记客户端
  • redirect_uri:可以理解一个 callback ,授权服务器验证完客户端与用户名等信息后将浏览器重定向到此地址并带上 code 参数
  • code:由授权服务器返回的一个凭证,用于获取AccessToken
  • state:由客户端传递给授权服务器,授权服务器一般到调用 redirect_uri 时原样返回

授权码授权请求

在使用授权码授权的模式中,作为客户端请求授权的的时候都需要按规范请求,以下是使用授权码授权发起授权时所需要的参数 :

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

如使用Github登陆Coding例子中的 https://github.com/login/oauth/authorize?client_id=a5ce5a6c7e8c39567ca0&scope=user:email&redirect_uri=https://coding.net/api/oauth/github/callback&response_type=code 授权请求URL,就有 client_id 、 redirect_uri 参数,至于为啥没有 response_type 在下猜想是因为Github给省了吧。

授权码授权响应

如果用户同意授权,那授权服务器也会返回标准的OAuth授权响应:

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

如Coding登陆中的 https://coding.net/api/oauth/github/callback&response_type=code ,用户同意授权后Github授权服务器回调Coding的回调地址,同时返回 code 、 state 参数。

客户端凭证授权

客房端凭证授权 授权的过程中只会涉及客户端与授权服务器交互,相比较其他三种授权类型是比较简单的。一般这种授权模式是用于服务之间授权,如在AWS中两台服务器分别为应用服务器(A)和数据服务器(B),A 服务器需要访问 B 服务器就需要通过授权服务器授权,然后才能去访问 B 服务器获取数据。

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

简单二步就可以完成 客房端凭证授权 啦,不过在使用 客房端凭证授权 时客户端是直接访问的授权服务器中获取AccessToken接口。

客户端凭证授权请求

客房端凭证授权中客户端会直接发起获取AccessToken请求授权服务器的 AccessToken Endpoint,请求参数如下:

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

注意:在OAuth中 AccessToken Endpoint是使用HTTP Basic认证,在请求时还需要携带 Authorization 请求头,如使用 postman 测试请求时:

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

其中的 username 和 password 参数对于OAuth协议中的 client_id 和 client_secret , client_id 和 client_secret 都是由授权服务器生成的。

客户端凭证授权响应

授权服务器验证完 client_id 和 client_secret 后返回token:

{   "access_token":"2YotnFZFEjr1zCsicMWpAA",   "token_type":"example",   "expires_in":3600,   "example_parameter":"example_value" }

用户凭证授权

用户凭证授权 与 客户端凭证授权 类似,不同的地方是进行授权时要提供用户名和用户的密码。

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

基本流程如下:

  • A、客户端首先需要知道用户的凭证
  • B、使用用户凭证获取AccessToken
  • C、授权服务器验证客户端与用户凭证,返回AccessToken

用户凭证授权请求

用户凭证授权请求参数要比客户端凭证授权多 username 和 pwssword 参数:

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

注意: 获取Token时使用HTTP Basic认证,与客户端凭证授权一样。

用户凭证授权响应

用户凭证授权响应与客户端凭证授权差不多:

{       "access_token":"2YotnFZFEjr1zCsicMWpAA",       "token_type":"example",       "expires_in":3600,       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",       "example_parameter":"example_value"     }

隐式授权

隐式授权用于获取AccessToken,但是获取的方式与 用户凭证授权 和 客户端授权 不同的是,它是在访问 授权Endpoint 的时候就会获取AccessToken而不是访问 Token Endpoing ,而且AccessToken的会作为 redirect_uri 的Segment返回。

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

  • A.1、A.2、浏览器访问支持隐式授权的服务器的授权Endpoint;
  • B.1、用户输入账号密码;
  • B.2、用户点击 授权按钮 ,同意授权;
  • C、授权服务器使用 redirect_uri 返回AccessToken;
  • D、授权服务器将浏览器重定向到 redirect_uri ,并携带AccessToken如: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600 ;
  • D、 redirect_uri 的地址是指向一个 Web资源客户端
  • E、 Web资源客户端 返回一段脚本
  • F、浏览器执行脚本
  • D、客户端获得AccessToken

隐式授权不太好理解,但是仔细比较 客户端凭证授权 和 用户凭证授权 会发现 隐式授权 不需要知道 用户凭证 或 客户端凭证 ,这样做相对更安全。

隐式授权请求

再使用 隐式授权 时,所需要请求参数如下:

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

隐式授权响应

隐式授权响应参数是通过 redirect_uri 回调返回的,如 http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600 就是隐式授权响应参数,其中需要注意的是响应的参数是使用Segment的形式的,而不是普通的URL参数。

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

OAuth2 客户端

前面提到过OAuth协议中有四个角色,这一节使用Spring Boot实现一个登陆 GitHub 的 OAuthClient ,要使用OAuth2协议登陆GitHub首先要云GitHub里面申请:

申请 OAuth App

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

填写必需的信息

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

上图中的 Authorization callback URL 就是 redirect_uri 用户同意授权后GitHub会将浏览器重定向到该地址,因此先要在本地的OAuth客户端服务中添加一个接口响应GitHub的重定向请求。

配置OAuthClient

熟悉OAuth2协议后,我们在使用 Spring Security OAuth2 配置一个GitHub授权客户端,使用 认证码 授权流程(可以先去看一遍认证码授权流程图),示例工程依赖:

<dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-oauth2-client</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>

Spring Security OAuth2 默认集成了Github、Goolge等常用的授权服务器,因为这些常用的授权服务的配置信息都是公开的,Spring Security OAuth2 已经帮我们配置了,开发都只需要指定必需的信息就行如:clientId、clientSecret。

Spring Security OAuth2 使用 Registration 作为客户端的的配置实体:

public static class Registration {    //授权服务器提供者名称    private String provider;    //客户端id    private String clientId;    //客户端凭证    private String clientSecret;      ....

下面是之前注册好的 GitHub OAuth App 的信息:

spring.security.oauth2.client.registration.github.clientId=5fefca2daccf85bede32spring.security.oauth2.client.registration.github.clientSecret=01dde7a7239bd18bd8a83de67f99dde864fb6524``

配置redirect_uri

Spring Security OAuth2 内置了一个redirect_uri模板: {baseUrl}/login/oauth2/code/{registrationId} ,其中的 registrationId 是在从配置中提取出来的:

spring.security.oauth2.client.registration.[registrationId].clientId=xxxxx

如在上面的GitHub客户端的配置中,因为指定的 registrationId 是 github ,所以重定向uri地址就是:

{baseUrl}/login/oauth2/code/github

启动服务器

OAuth2客户端和重定向Uri配置好后,将服务器启动,然后打开浏览器进入: http://localhost:8080/ 。第一次打开因为没有认证会将浏览器重客向到GitHub的 授权Endpoint :

一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成

 

常用授权服务器(CommonOAuth2Provider)

Spring Security OAuth2 内置了一些常用的授权服务器的配置,这些配置都在 CommonOAuth2Provider 中:

public enum CommonOAuth2Provider {    GOOGLE {        @Override        public Builder getBuilder(String registrationId) {            ClientRegistration.Builder builder = getBuilder(registrationId,                    ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);            builder.scope("openid", "profile", "email");            builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");            builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");            builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");            builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");            builder.userNameAttributeName(IdTokenClaimNames.SUB);            builder.clientName("Google");            return builder;        }    },    GITHUB {        @Override        public Builder getBuilder(String registrationId) {            ClientRegistration.Builder builder = getBuilder(registrationId,                    ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);            builder.scope("read:user");            builder.authorizationUri("https://github.com/login/oauth/authorize");            builder.tokenUri("https://github.com/login/oauth/access_token");            builder.userInfoUri("https://api.github.com/user");            builder.userNameAttributeName("id");            builder.clientName("GitHub");            return builder;        }    },    FACEBOOK {        @Override        public Builder getBuilder(String registrationId) {            ClientRegistration.Builder builder = getBuilder(registrationId,                    ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL);            builder.scope("public_profile", "email");            builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");            builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");            builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");            builder.userNameAttributeName("id");            builder.clientName("Facebook");            return builder;        }    },    OKTA {        @Override        public Builder getBuilder(String registrationId) {            ClientRegistration.Builder builder = getBuilder(registrationId,                    ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);            builder.scope("openid", "profile", "email");            builder.userNameAttributeName(IdTokenClaimNames.SUB);            builder.clientName("Okta");            return builder;        }    };    private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";}

CommonOAuth2Provider 中有四个授权服务器配置: OKTA 、 FACEBOOK 、 GITHUB 、 GOOGLE 。在OAuth2协议中的配置项 redirect_uri 、 Token Endpoint 、 授权 Endpoint 、 scope 都会在这里配置:

GITHUB {        @Override        public Builder getBuilder(String registrationId) {            ClientRegistration.Builder builder = getBuilder(registrationId,                    ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);            builder.scope("read:user");            builder.authorizationUri("https://github.com/login/oauth/authorize");            builder.tokenUri("https://github.com/login/oauth/access_token");            builder.userInfoUri("https://api.github.com/user");            builder.userNameAttributeName("id");            builder.clientName("GitHub");            return builder;        }    }

重定向Uri拦截

脑瓜子有点蒙了,感觉自己就配置了 clientid 和 clientSecret 一个OAuth2客户端就完成了,其中的一些原由还没搞明白啊。。。,最好奇的是重定向Uri是怎么被处理的。

Spring Security OAuth2 是基于 Spring Security 的,之前看过 Spring Security 文章,知道它的处理原理是基于过滤器的,如果你不知道的话推荐看这篇文章: 《Spring Security 架构》 。在源码中找了一下,发现一个可疑的Security 过滤器:

  • OAuth2LoginAuthenticationFilter:处理OAuth2授权的过滤器

这个 Security 过滤器有个常量:

public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";

是一个匹配器,之前提到过 Spring Security OAuth2 中有一个默认的redirect_uri模板: {baseUrl}/{action}/oauth2/code/{registrationId} , /login/oauth2/code/* 正好能与redirect_uri模板匹配成功,所以 OAuth2LoginAuthenticationFilter 会在用户同意授权后执行,它的构造方法如下:

public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository,                                        OAuth2AuthorizedClientService authorizedClientService) {    this(clientRegistrationRepository, authorizedClientService, DEFAULT_FILTER_PROCESSES_URI);}

OAuth2LoginAuthenticationFilter 主要将授权服务器返回的 code 拿出来,然后通过AuthenticationManager 来认证(获取AccessToken),下来是移除部分代码后的源代码:

@Override    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)            throws AuthenticationException {        MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());        //检查没code与state        if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {            OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());        }        //获取 OAuth2AuthorizationRequest         OAuth2AuthorizationRequest authorizationRequest =                this.authorizationRequestRepository.removeAuthorizationRequest(request, response);        if (authorizationRequest == null) {            OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());        }         //取出 ClientRegistration          String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);        if (clientRegistration == null) {            OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,                    "Client Registration not found with Id: " + registrationId, null);            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());        }        String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))                .replaceQuery(null)                .build()                .toUriString();                        //认证、获取AccessToken        OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);        Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);        OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(                clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));        authenticationRequest.setDetails(authenticationDetails);        OAuth2LoginAuthenticationToken authenticationResult =            (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);        ...        return oauth2Authentication;    }

获取AccessToken

前面提到 OAuth2LoginAuthenticationFilter 是使用 AuthenticationManager 来进行OAuth2认证的,一般情况下在 Spring Security 中的 AuthenticationManager 都是使用的 ProviderManager 来进行认证的,所以对应在 Spring Security OAuth2 中有一个 OAuth2LoginAuthenticationProvider 用于获取AccessToken:

public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {    private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;    private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;    private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);    ....        @Override    public Authentication authenticate(Authentication authentication) throws AuthenticationException {        OAuth2LoginAuthenticationToken authorizationCodeAuthentication =            (OAuth2LoginAuthenticationToken) authentication;        // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest        // scope        //      REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.        if (authorizationCodeAuthentication.getAuthorizationExchange()            .getAuthorizationRequest().getScopes().contains("openid")) {            // This is an OpenID Connect Authentication Request so return null            // and let OidcAuthorizationCodeAuthenticationProvider handle it instead            return null;        }        OAuth2AccessTokenResponse accessTokenResponse;        try {            OAuth2AuthorizationExchangeValidator.validate(                    authorizationCodeAuthentication.getAuthorizationExchange());                               //访问GitHub TokenEndpoint获取Token            accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(                    new OAuth2AuthorizationCodeGrantRequest(                            authorizationCodeAuthentication.getClientRegistration(),                            authorizationCodeAuthentication.getAuthorizationExchange()));        } catch (OAuth2AuthorizationException ex) {            OAuth2Error oauth2Error = ex.getError();            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());        }         ...        return authenticationResult;    }    @Override    public boolean supports(Class<?> authentication) {        return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);    }}

参考资料

  • OAuth 2 Developers Guide
  • draft-ietf-oauth-v

OAuth2.0协议专区-Springcloud集成springsecurity oauth2实现服务统一认证,应该时最简单的教程了~

OAuth2.0协议专区-Springcloud集成springsecurity oauth2实现服务统一认证,应该时最简单的教程了~

1.配置认证服务器

(1) 首先配置springsecurity,其实他底层是很多filter组成,顺序是请求先到他这里进行校验,然后在到oauth

/**

 * @author: gaoyang

 * @Description: 身份认证拦截

 */

@Order(1)

@Configuration

//注解权限拦截

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

 

    @Autowired

    UserDetailsServiceConfig userDetailsServiceConfig;

 

    //认证服务器需配合Security使用

    @Bean

    @Override

    public AuthenticationManager authenticationManagerBean() throws Exception {

        return super.authenticationManagerBean();

    }

//websecurity用户密码和认证服务器客户端密码都需要加密算法 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //验证用户权限 auth.userDetailsService(userDetailsServiceConfig); //也可以在内存中创建用户并为密码加密 // auth.inMemoryAuthentication() // .withUser("user").password(passwordEncoder().encode("123")).roles("USER") // .and() // .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN"); } //uri权限拦截,生产可以设置为启动动态读取数据库,具体百度 @Override protected void configure(HttpSecurity http) throws Exception { http //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态 .formLogin() .and() .csrf().disable() .authorizeRequests().antMatchers("/test").permitAll() .and() .authorizeRequests().anyRequest().authenticated(); } //设置不拦截资源服务器的认证请求 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/oauth/check_token"); } }

  (2)这里的UserDetailsServiceConfig就是去校验登陆用户,可以写测试使用内存或者数据库方式读取用户信息(我这里写死了账号为user,密码为123)

@Component
public class UserDetailsServiceConfig implements UserDetailsService {

  @Autowired
  private PasswordEncoder passwordEncoder;

  //生产环境使用数据库进行验证
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    if (!username.equals("user")) {
      throw new AcceptPendingException();
    }
    return new User(username, passwordEncoder.encode("123"),
    AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
  }
}

(3)配置认证服务器(详见注释)

/**
 * @author: gaoyang
 * @Description:认证服务器配置
 */
@Order(2)
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    UserDetailsServiceConfig myUserDetailsService;
 
    //为了测试客户端与凭证存储在内存(生产应该用数据库来存储,oauth有标准数据库模板)
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client1-code") // client_id
                .secret(bCryptPasswordEncoder.encode("123")) // client_secret
                .authorizedGrantTypes("authorization_code") // 该client允许的授权类型
                .scopes("app") // 允许的授权范围
                .redirectUris("https://www.baidu.com")
                .resourceIds("goods", "mechant")    //资源服务器id,需要与资源服务器对应
 
                .and()
                .withClient("client2-credentials")
                .secret(bCryptPasswordEncoder.encode("123"))
                .authorizedGrantTypes("client_credentials")
                .scopes("app")
                .resourceIds("goods", "mechant")
 
                .and()
                .withClient("client3-password")
                .secret(bCryptPasswordEncoder.encode("123"))
                .authorizedGrantTypes("password")
                .scopes("app")
                .resourceIds("mechant")
 
                .and()
                .withClient("client4-implicit")
                .authorizedGrantTypes("implicit")
                .scopes("app")
                .resourceIds("mechant");
    }
 
    //配置token仓库
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //authenticationManager配合password模式使用
        endpoints.authenticationManager(authenticationManager)
                //这里使用内存存储token,也可以使用redis和数据库
                .tokenStore(new InMemoryTokenStore());
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
        endpoints.tokenEnhancer(new TokenEnhancer() {
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
                //在返回token的时候可以加上一些自定义数据
                DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken;
                Map<String, Object> map = new LinkedHashMap<>();
                map.put("nickname", "测试姓名");
                token.setAdditionalInformation(map);
                return token;
            }
        });
    }
 
    //配置token状态查询
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求
        security.allowFormAuthenticationForClients();
    }
 
    //以下数据库配置
    /**
     *
     *     @Bean
     *     @Primary
     *     @ConfigurationProperties(prefix = "spring.datasource")
     *     public DataSource dataSource() {
     *         // 配置数据源(注意,我使用的是 HikariCP 连接池),以上注解是指定数据源,否则会有冲突
     *         return DataSourceBuilder.create().build();
     *     }
     *
     *     @Bean
     *     public TokenStore tokenStore() {
     *         // 基于 JDBC 实现,令牌保存到数据
     *         return new JdbcTokenStore(dataSource());
     *     }
     *
     *     @Bean
     *     public ClientDetailsService jdbcClientDetails() {
     *         // 基于 JDBC 实现,需要事先在数据库配置客户端信息
     *         return new JdbcClientDetailsService(dataSource());
     *     }
     *
     *     @Override
     *     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
     *         // 设置令牌
     *         endpoints.tokenStore(tokenStore());
     *     }
     *
     *     @Override
     *     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     *         // 读取客户端配置
     *         clients.withClientDetails(jdbcClientDetails());
     *     }
     *
     */
}

(4) 新增自定义返回认证服务器数据:(这里只做演示,没有合理封装)

@RestController
@RequestMapping("/oauth")
public class CustomResult {
 
    @Autowired
    private TokenEndpoint tokenEndpoint;
 
    @GetMapping("/token")
    public Object getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        return this.result(principal,parameters);
    }
 
    @PostMapping("/token")
    public Object postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        return this.result(principal,parameters);
    }
 
    public Object result(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.getAccessToken(principal, parameters);
        OAuth2AccessToken body = accessToken.getBody();
        Map<String, Object> customMap = body.getAdditionalInformation();
        String value = body.getValue();
        OAuth2RefreshToken refreshToken = body.getRefreshToken();
        Set<String> scope = body.getScope();
        int expiresIn = body.getExpiresIn();
        customMap.put("token",value);
        customMap.put("scope",scope);
        customMap.put("expiresIn",expiresIn);
        customMap.put("refreshToken",refreshToken);
        Map map = new HashMap();
        map.put("code",0);
        map.put("msg","success");
        map.put("data",customMap);
        return map;
    }
}

  

(5)添加获取token错误返回:(注意,客户端信息错误这里是拦截不到的)

@RestControllerAdvice
public class RestControllerExceptionAdvice {
 
    //判断oauth异常,自定义返回数据
    @ExceptionHandler
    public Object exception(OAuth2Exception e){
        //if ("invalid_client".equals(errorCode)) {
        //            return new InvalidClientException(errorMessage);
        //        } else if ("unauthorized_client".equals(errorCode)) {
        //            return new UnauthorizedClientException(errorMessage);
        //        } else if ("invalid_grant".equals(errorCode)) {
        //            return new InvalidGrantException(errorMessage);
        //        } else if ("invalid_scope".equals(errorCode)) {
        //            return new InvalidScopeException(errorMessage);
        //        } else if ("invalid_token".equals(errorCode)) {
        //            return new InvalidTokenException(errorMessage);
        //        } else if ("invalid_request".equals(errorCode)) {
        //            return new InvalidRequestException(errorMessage);
        //        } else if ("redirect_uri_mismatch".equals(errorCode)) {
        //            return new RedirectMismatchException(errorMessage);
        //        } else if ("unsupported_grant_type".equals(errorCode)) {
        //            return new UnsupportedGrantTypeException(errorMessage);
        //        } else if ("unsupported_response_type".equals(errorCode)) {
        //            return new UnsupportedResponseTypeException(errorMessage);
        //        } else {
        //            return (OAuth2Exception)("access_denied".equals(errorCode) ? new UserDeniedAuthorizationException(errorMessage) : new OAuth2Exception(errorMessage));
        //        }
        return "获取token错误";
    }
}

(6)添加自定义登陆及授权页面:

@Controller
// 必须配置该作用域设置
@SessionAttributes("authorizationRequest")
public class Oauth2Controller {
 
 
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
 
    @RequestMapping("/authentication/require")
    @ResponseBody
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public Map requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
 
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (null != savedRequest) {
            String targetUrl = savedRequest.getRedirectUrl();
            System.out.println("引发跳转的请求是:" + targetUrl);
            redirectStrategy.sendRedirect(request, response, "/ologin");
        }
        //如果访问的是接口资源
        return new HashMap() {{
            put("code", 401);
            put("msg", "访问的服务需要身份认证,请引导用户到登录页");
        }};
    }
 
    @RequestMapping("/ologin")
    public String oauthLogin(){
        return "oauthLogin";
    }
 
    //授权控制器
    @RequestMapping("/oauth/confirm_access")
    public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
        Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ?
                model.get("scopes") : request.getAttribute("scopes"));
        List<String> scopeList = new ArrayList<>();
        if (scopes != null) {
            scopeList.addAll(scopes.keySet());
        }
        model.put("scopeList", scopeList);
        return "oauthGrant";
    }
}

  

//uri权限拦截,生产可以设置为启动动态读取数据库,具体百度
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态
                .formLogin().loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .passwordParameter("password")
                .usernameParameter("username")
                .and()
                .csrf().disable()
 
                .authorizeRequests().antMatchers("/test","/authentication/require","/ologin").permitAll()
                .and()
                .authorizeRequests().anyRequest().authenticated();
    }

4.配置资源服务器

(1)配置

//配置资源服务器
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
 
    private ObjectMapper objectMapper = new ObjectMapper();
 
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //设置资源服务器id,需要与认证服务器对应
        resources.resourceId("mechant");
        //当权限不足时返回
        resources.accessDeniedHandler((request, response, e) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.getWriter()
                    .write(objectMapper.writeValueAsString(Result.from("0001", "权限不足", null)));
        });
        //当token不正确时返回
        resources.authenticationEntryPoint((request, response, e) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.getWriter()
                    .write(objectMapper.writeValueAsString(Result.from("0002", "access_token错误", null)));
        });
    }
    
    //配置uri拦截策略 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic().disable()
                .exceptionHandling()
                .authenticationEntryPoint((req, resp, exception) -> {
                    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    resp.getWriter()
                            .write(objectMapper.writeValueAsString(Result.from("0002", "没有携带token", null)));
                })
                .and()
                //无需登陆
                .authorizeRequests().antMatchers("/noauth").permitAll()
                .and()
                //拦截所有请求,并且检查sope
                .authorizeRequests().anyRequest().access("isAuthenticated() && #oauth2.hasScope(''app'')");
    }
 
    //静态内部返回类
    @Data
    static class Result<T> {
        private String code;
        private String msg;
        private T data;
 
        public Result(String code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
 
        public static <T> Result from(String code, String msg, T data) {
            return new Result(code, msg, data);
        }
    }
}

(2)测试接口

@RestController
public class TestController {
 
    @GetMapping("ping")
    public Object test() {
        return "pong";
    }
   
    //无需登陆
    @GetMapping("noauth")
    public Object noauth() {
        return "noauth";
    }
 
}

(3)application.yml配置(远程向认证服务器鉴权)

#配置向认证服务器认证权限
security:
  oauth2:
    client:
      client-id: client3-password
      client-secret: 123
      access-token-uri: http://localhost:8082/oauth/token
      user-authorization-uri: http://localhost:8082/oauth/authorize
    resource:
      token-info-uri: http://localhost:8082/oauth/check_token

5.测试用例~

(1)password模式

  表单方式:(localhost:8082/oauth/token?username=user&password=123&grant_type=password&client_secret=123&client_id=client3-password)

注意需要开启认证服务器的:

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求
  security.allowFormAuthenticationForClients();
}

表单加token方式:

 

(2)code模式

  浏览器访问:  localhost:8082/oauth/authorize?client_id=client1-code&response_type=code

跳转到登陆页面:

 

 

 

选择允许

 

 

 

 

然后跳转到之前设置的地址,并携带code:

 

 

 

 

 

拿着code请求token:

 

 

自定义登陆及授权页面:

 

 

## 当前这样配置的话,如果各微服务之间互相调用,则是没有权限的;所以我们可以给他加上token,例如使用feign:

@Component
public class OauthConfig implements RequestInterceptor {
  @Override
  public void apply(RequestTemplate requestTemplate) {
    requestTemplate.query("access_token","71f422b5-2204-4653-8a85-9cf2c62aac81");
  }
}


  这里我写固定了,其实实现的话可以在认证服务器指定一个客户端模式,然后去动态获取token,这个token的过期缓存等等,可以自由发挥;

  其实现在很多微服务都是内网通信,通过路由暴露端口了,所以可以定制一些特殊请求,来做无权限访问?

 

Spring Boot HttpSecurity - @PreAuthorize - 如何设置 AuthenticationFilter? 使用 Spring Security 的内置 JWT 支持无需授权服务器即可使用 Spring Security 的内置 JWT 支持自己动手

Spring Boot HttpSecurity - @PreAuthorize - 如何设置 AuthenticationFilter? 使用 Spring Security 的内置 JWT 支持无需授权服务器即可使用 Spring Security 的内置 JWT 支持自己动手

JwtAuthorizationFilter 的目的应该是设置 Authentication 的授予权限。那么,Spring Security 的默认方法 security 就足够了。

您有几个选择:

使用 Spring Security 的内置 JWT 支持

如果 JWT 是由授权服务器铸造的,那么 Spring Security's default JWT support 可能就足够了。 Spring Security 附带 BearerTokenAuthenticationFilterJwtAuthenticationProvider。过滤器将解析 Authorization 标头以获取令牌,提供程序将验证令牌并构建基于 Authenticationscope 声明的 scp。在这种情况下,您可以执行类似 @PreAuthorize("hasAuthority('SCOPE_ADMIN')") 的操作。

如果您需要自定义如何将 JWT 声明转换为 GrantedAuthority,那么您可以发布一个 JwtAuthenticationConverter @Bean

有关完整的设置详细信息,请查看 Spring Security's OAuth 2.0 Resource Server sample。不过,基本上,您的配置如下所示:

http
    .authorizeRequests((authz) -> authz
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer((oauth2) -> oauth2
        .jwt(Customizer.withDefaults())
    );

无需授权服务​​器即可使用 Spring Security 的内置 JWT 支持

Spring Security 的现有支持在设计时考虑了授权服务器,但这不是必需的。

您可以改为让您的应用程序 mint self-signed tokens。

自己动手

您可以保留您的 JwtAuthorizationFilter,但尚不清楚 Spring Security 现有支持为何不足。要添加过滤器,您只需执行 addFilterBefore(myFilter,BearerTokenAuthenticationFilter.class)。您可以查看 BearerTokenAuthenicationFilter 作为创建自己的过滤器时应考虑的一些事项的示例。

Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

标题文字 #### 概要

之前的两篇文章,讲述了Spring Security 结合 OAuth2 、JWT 的使用,这一节要求对 OAuth2、JWT 有了解,若不清楚,先移步到下面两篇提前了解下。

Spring Boot Security 整合 OAuth2 设计安全API接口服务

Spring Boot Security 整合 JWT 实现 无状态的分布式API接口

这一篇我们来实现 支持 JWT令牌 的授权服务器。

优点

使用 OAuth2 是向认证服务器申请令牌,客户端拿这令牌访问资源服务服务器,资源服务器校验了令牌无误后,如果资源的访问用到用户的相关信息,那么资源服务器还需要根据令牌关联查询用户的信息。

使用 JWT 是客户端通过用户名、密码 请求服务器获取 JWT,服务器判断用户名和密码无误之后,可以将用户信息和权限信息经过加密成 JWT 的形式返回给客户端。在之后的请求中,客户端携带 JWT 请求需要访问的资源,如果资源的访问用到用户的相关信息,那么就直接从JWT中获取到。

所以,如果我们在使用 OAuth2 时结合JWT ,就能节省集中式令牌校验开销,实现无状态授权认证。

快速上手

项目说明

工程名 端口 作用
jwt-authserver 8080 授权服务器
jwt-resourceserver 8081 资源服务器

授权服务器

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.10.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

WebSecurityConfig

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
             authorizeRequests().antMatchers("/**").permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("user").password("123456").roles("USER");
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return Objects.equals(charSequence.toString(),s);
            }
        };
    }

}

为了方便,使用内存模式,在内存中创建一个用户 user 密码 123456。

OAuth2AuthorizationServer

/**
 * 授权服务器
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    /**
     * 注入AuthenticationManager ,密码模式用到
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 对Jwt签名时,增加一个密钥
     * JwtAccessTokenConverter:对Jwt来进行编码以及解码的类
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("test-secret");
        return converter;
    }

    /**
     * 设置token 由Jwt产生,不使用默认的透明令牌
     */
    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore())
                .accessTokenConverter(accessTokenConverter());
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("clientapp")
                .secret("123")
                .scopes("read")
                //设置支持[密码模式、授权码模式、token刷新]
                .authorizedGrantTypes(
                        "password",
                        "authorization_code",
                        "refresh_token");
    }


}

资源服务器

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.10.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

HelloController

@RestController("/api")
public class HelloController {

    @PostMapping("/api/hi")
    public String say(String name) {
        return "hi , " + name;
    }

}

OAuth2ResourceServer

/**
 * 资源服务器
 */
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated().and()
            .requestMatchers().antMatchers("/api/**");
    }
}

application.yml

server:
  port: 8081

security:
  oauth2:
    resource:
      jwt:
        key-value: test-secret

参数说明:

  • security.oauth2.resource.jwt.key-value:设置签名key 保持和授权服务器一致。
  • security.oauth2.resource.jwt:项目启动过程中,检查到配置文件中有

security.oauth2.resource.jwt 的配置,就会生成 jwtTokenStore 的 bean,对令牌的校验就会使用 jwtTokenStore 。

验证

请求令牌

curl -X POST --user ''clientapp:123'' -d ''grant_type=password&username=user&password=123456'' http://localhost:8080/oauth/token

返回JWT令牌

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiI4YzRhYzI5Ni0wNDBhLTRjZTMtODkxMC0xYmY2NmRhNDA5OTciLCJleHAiOjE1NTY5Nzk5MDgsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI0ZjA5M2ZjYS04NmM0LTQxZWUtODcxZS1kZTY2ZjFhOTI0NTAiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.vvAE2LcqggBv8pxuqU6RKPX65bl7Zl9dfcoIbIQBLf4",
    "expires_in": 43199,
    "scope": "read",
    "jti": "8c4ac296-040a-4ce3-8910-1bf66da40997"
}

携带JWT令牌请求资源

curl -X POST -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU" -d ''name=zhangsan'' http://localhost:8081/api/hi

返回

hi , zhangsan

源码

https://github.com/gf-huanchu...

欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~

关于Spring Security 实现 OAuth2.0 授权服务 - 进阶版spring security授权服务器的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于OAuth2 协议与 Spring Security OAuth2 集成、OAuth2.0协议专区-Springcloud集成springsecurity oauth2实现服务统一认证,应该时最简单的教程了~、Spring Boot HttpSecurity - @PreAuthorize - 如何设置 AuthenticationFilter? 使用 Spring Security 的内置 JWT 支持无需授权服务器即可使用 Spring Security 的内置 JWT 支持自己动手、Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器等相关知识的信息别忘了在本站进行查找喔。

本文标签: