SpringBoot从入门到精通教程(二十二)- Oauth2+Token详细用法/SpringSecurity

需求背景

本篇文章讲解如何通过Springboot2集成验证服务Token,以及资源服务的用法(更多官方关于 Oauth2

概要

主要使用Spring Boot2和SpringSecurity5

OAuth2术语

  • 资源拥有者 Resource Owner
    • 用户授权哪些应用程序,能够去访问资源信息等,访问受限于作用域
  • 资源服务 Resource Server
    • 一个在客户端拥有令牌后,处理验证它请求的服务
  • 客户端 Client
    • 一个代表资源拥有者的应用程序,可以访问受保护的资源
  • 授权服务 Authorization Server
    • 一个在成功验证了客户端和资源拥有者,以及验证请求后,分发令牌的授权服务
  • 令牌 Access Token
    • 一个用于访问受限资源的唯一token
  • 作用域范围 Scope
    • 一个许可权限
  • 授权类型 Grant type

Oauth2 密码授权流程

在oauth2协议中,一个应用会有自己的clientId和clientSecret(从认证方申请),由认证方下发clientId和secret

代码演示

授权服务 Authorization Server

构建Authorization Server,这里我使用了SpringSecurity5 和 SpringBoot 2.0.6版本

1. 项目目录结构

2. pom.xml依赖组件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib-ext-spring</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

3. 这里为演示,使用了H2数据库

以下是Spring Security需要用到的OAuth2 SQL

CREATE TABLE IF NOT EXISTS oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256) NOT NULL,
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4000),
  autoapprove VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256),
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

CREATE TABLE IF NOT EXISTS oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

CREATE TABLE IF NOT EXISTS oauth_code (
  code VARCHAR(256), authentication BLOB
);

使用下面这句:

INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
  VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);

特别要注意:这里是下发clientId和secret,值分别为clientId和secret(这两个值后面要用到)

这里的client_secret列值,由bcrypt生成

{bcrypt}前缀写法,这是Spring Security 5 DelegatingPasswordEncoder的新用法

access_token_validity列:表示token有效时间,单位秒,即300秒有效

同时,以下是User和Authority表(被org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl使用到)

CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(256) NOT NULL,
  password VARCHAR(256) NOT NULL,
  enabled TINYINT(1),
  UNIQUE KEY unique_username(username)
);

CREATE TABLE IF NOT EXISTS authorities (
  username VARCHAR(256) NOT NULL,
  authority VARCHAR(256) NOT NULL,
  PRIMARY KEY(username, authority)
);

同样,使用下面的sql,初始化user和authority

INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');

注意:用户名是user,password的值是:pass,后面会用到

Spring Security Configuration 部分

4. WebSecurityConfiguration.java文件

package com.md.demo.oauth.opaque.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.sql.DataSource;

@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

	private final DataSource dataSource;

	private PasswordEncoder passwordEncoder;
	private UserDetailsService userDetailsService;

	public WebSecurityConfiguration(final DataSource dataSource) {
		this.dataSource = dataSource;
	}

	// 排除路径验证
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/hello");
	}

	@Override
	protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
	}

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

	@Bean
	public PasswordEncoder passwordEncoder() {
		if (passwordEncoder == null) {
			passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
		}
		return passwordEncoder;
	}

	@Bean
	@Override
	public UserDetailsService userDetailsService() {
		if (userDetailsService == null) {
			userDetailsService = new JdbcDaoImpl();
			((JdbcDaoImpl) userDetailsService).setDataSource(dataSource);
		}
		return userDetailsService;
	}

}

一起使用@EnableWebSecurity注解和WebSecurityConfigurerAdapter,可以提供web基础安全机制

5. 附加说明

如果你使用了springboot DataSource数据源对象,它会自动装配,你不需要再自定义,只需要注入即可。它需要注入到由Spring Security提供的UserDetailsService中,结合使用JdbcDaoImpl。如果需要,你也能替换它去自定义实现。

6. 验证服务配置

验证服务验证客户端和用户凭证,同时生成token

package com.md.demo.oauth.opaque.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;

    private TokenStore tokenStore;

    public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
                                            final AuthenticationManager authenticationManager) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
    }

    @Bean
    public TokenStore tokenStore() {
        if (tokenStore == null) {
            tokenStore = new JdbcTokenStore(dataSource);
        }
        return tokenStore;
    }

    @Bean
    public DefaultTokenServices tokenServices(final ClientDetailsService clientDetailsService) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setAuthenticationManager(authenticationManager);
        return tokenServices;
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore());
    }

    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer.passwordEncoder(passwordEncoder)
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

}

7. 测试接口访问

package com.md.demo.oauth.rest;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("/profile")
public class ProfileController {

    //http://localhost:9001/profile/me?access_token=xxxxxx
    @GetMapping("/me")
    public ResponseEntity<Principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }

    /*
	 * 通过以下接口Post请求获得access_token:
	 * http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
	 * 
	 * 注意:Authorization中使用BasicAuth,Username和Password分别为:clientId,secret
	 */
}

资源服务 Resource Server

1. 项目目录结构

2. pom.xml依赖组件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib-ext-spring</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
</dependency>

3. 接口访问(受保护)

package com.md.demo.oauth.rest;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("/me")
public class UserController {

    //http://localhost:9101/me?access_token=xxxxxx
    @GetMapping
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<Principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }

    /*
	 * 通过以下接口Post请求获得access_token:
	 * http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
	 * 
	 * 注意:Authorization中使用BasicAuth,Username和Password分别为:clientId,secret
	 */
}

@PreAuthorize注解验证用户的权限去是否执行代码,同时需要启用prePost注解

4. Web安全配置

package com.md.demo.oauth.opaque.ds.config;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {

}

prePostEnabled默认是false,使@PreAuthorize生效,则设为true

5. 资源服务配置

package com.md.demo.oauth.opaque.ds.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

	private static final String ROOT_PATTERN = "/**";

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				// 排除路径验证
				.antMatchers("/hello")
					.permitAll()
				.antMatchers(ROOT_PATTERN)
					.authenticated();
	}
}

6. application.yml配置

server:
  port: 9101

# 设置url,把token转换成authentication对象
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:9001/profile/me

spring:
  jackson:
    serialization:
      INDENT_OUTPUT: true

测试访问

1. 访问受保护接口:http://localhost:9101/me,默认失败

2. 获得token接口:http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass

3. 再访问受保护接口:http://localhost:9101/me?access_token=8df31c53-e616-47d1-802e-9b00adf64266,即可访问成功

完整源码下载

我的Github源码仓库地址:

下一章教程

SpringBoot从入门到精通教程(二十三)- Oauth2+JWT集成/SpringSecurity

该系列教程

SpringBoot从入门到精通教程

 

 

 

至此,全部介绍就结束了

 

------------------------------------------------------

------------------------------------------------------

 

关于我(个人域名)

我的开源项目集Github

 

期望和大家一起学习,一起成长,共勉,O(∩_∩)O谢谢

欢迎交流问题,可加个人QQ 469580884,

或者,加我的群号 751925591,一起探讨交流问题

不讲虚的,只做实干家

Talk is cheap,show me the code

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页