前言:本文是OAuth2.0实践篇,阅读之前需要先掌握OAuth2.0基本原理,原理介绍见:OAuth2.0入门(一)—— 基本概念详解和图文并茂讲解四种授权类型
本章将采用微服务架构方式,将OAuth2-Demo拆分成三个模块:oauth2-authentication-server(作为授权认证中心)、oauth2-resource-server(作为资源服务器)、oauth-client(作为第三方应用,模拟如何获取Token访问资源)。
其中oauth2-demo是其他模块的Parent模块,定义了一些通用的Jar包。完整的pom文件如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
com.scb
oauth2-demo
0.0.1-SNAPSHOT
oauth2-demo
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter
ch.qos.logback
logback-classic
1.1.11
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
oauth2-authentication-server
oauth2-resource-server
oauth-client
oauth2-authentication-server 模块是作为全局的授权认证中心,pom文件如下:
4.0.0
com.scb
oauth2-demo
0.0.1-SNAPSHOT
oauth2-authentication-server
0.0.1-SNAPSHOT
oauth2-authentication-server
Demo project for Spring Boot
1.8
com.alibaba
druid-spring-boot-starter
1.1.9
log4j
log4j
1.2.17
com.h2database
h2
runtime
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
org.springframework.security
spring-security-test
test
org.springframework.boot
spring-boot-maven-plugin
这里,除了导入spring-boot-starter-security和spring-security-oauth2认证框架外,还需要使用H2内存数据库来存储用户和角色信息及OAuth2的表。
先来看看application.yml文件:
spring:
h2:
console:
path: /h2-console
enabled: true
settings:
web-allow-others: true
jpa:
generate-ddl: false
show-sql: true
hibernate:
ddl-auto: none
datasource:
platform: h2
schema: classpath:schema.sql
data: classpath:data.sql
url: jdbc:h2:~/auth;AUTO_SERVER=TRUE
username: sa
password:
type: com.alibaba.druid.pool.DruidDataSource
druid:
min-idle: 2
initial-size: 5
max-active: 10
max-wait: 5000
validation-query: select 1
resources:
static-locations: classpath:/templates/,classpath:/static/
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML5
servlet:
content-type: text/html
cache: false
server:
port: 8080
logging:
pattern:
level: debug
在yml文件中,我们定义了datasource为H2,并指定了schema、data文件,这样在项目运行时会执行相应的sql。其中schema.sql文件如下:
/* 1、存放用户认证信息及权限 */
drop table if exists authority;
CREATE TABLE authority (
id integer,
authority varchar(255),
primary key (id)
);
drop table if exists credentials;
CREATE TABLE credentials (
id integer,
enabled boolean not null,
name varchar(255) not null,
password varchar(255) not null,
version integer,
primary key (id)
);
drop table if exists credentials_authorities;
CREATE TABLE credentials_authorities (
credentials_id bigint not null,
authorities_id bigint not null
);
/* 2、oauth2官方建表语句 */
drop table if exists oauth_client_token;
create table oauth_client_token (
token_id VARCHAR(255),
token LONGBLOB,
authentication_id VARCHAR(255),
user_name VARCHAR(255),
client_id VARCHAR(255)
);
drop table if exists oauth_client_details;
CREATE TABLE oauth_client_details (
client_id varchar(255) NOT NULL,
resource_ids varchar(255) DEFAULT NULL,
client_secret varchar(255) DEFAULT NULL,
scope varchar(255) DEFAULT NULL,
authorized_grant_types varchar(255) DEFAULT NULL,
web_server_redirect_uri varchar(255) DEFAULT NULL,
authorities varchar(255) DEFAULT NULL,
access_token_validity integer(11) DEFAULT NULL,
refresh_token_validity integer(11) DEFAULT NULL,
additional_information varchar(255) DEFAULT NULL,
autoapprove varchar(255) DEFAULT NULL
);
drop table if exists oauth_access_token;
create table `oauth_access_token` (
token_id VARCHAR(255),
token LONGBLOB,
authentication_id VARCHAR(255),
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication LONGBLOB,
refresh_token VARCHAR(255)
);
drop table if exists oauth_refresh_token;
create table `oauth_refresh_token`(
token_id VARCHAR(255),
token LONGBLOB,
authentication LONGBLOB
);
drop table if exists oauth_code;
create table oauth_code (
code VARCHAR(255),
authentication BLOB
);
drop table if exists oauth_approvals;
create table oauth_approvals (
userId VARCHAR(255),
clientId VARCHAR(255),
scope VARCHAR(255),
status VARCHAR(10),
expiresAt DATETIME,
lastModifiedAt DATETIME
);
这里分为两部分,第一部分是自定义的用于存放用户凭证及授权的表。第二部分是官方建表语句:spring-security-oauth schema.sql,各个数据表说明如下:
字段及表详细说明如下,图片来源:https://blog.csdn.net/qq_34997906/article/details/89609297
再来看一下data.sql文件,该文件主要是创建一些初始数据。
INSERT INTO authority VALUES(1,'ROLE_OAUTH_ADMIN');
INSERT INTO authority VALUES(2,'ROLE_RESOURCE_ADMIN');
INSERT INTO authority VALUES(3,'ROLE_PRODUCT_ADMIN');
/* password ==> password */
INSERT INTO credentials VALUES(1,true,'oauth_admin','$2a$10$5ze/vcFOsQBF1og.s.eQ0.8VdsUXh7zzul8VM0Dzcq/NKVNrD8ffO','0');
INSERT INTO credentials VALUES(2,true,'resource_admin','$2a$10$5ze/vcFOsQBF1og.s.eQ0.8VdsUXh7zzul8VM0Dzcq/NKVNrD8ffO','0');
INSERT INTO credentials VALUES(3,true,'product_admin','$2a$10$5ze/vcFOsQBF1og.s.eQ0.8VdsUXh7zzul8VM0Dzcq/NKVNrD8ffO','0');
INSERT INTO credentials_authorities VALUES (1,1);
INSERT INTO credentials_authorities VALUES (2,2);
INSERT INTO credentials_authorities VALUES (3,3);
/* password ==> password */
INSERT INTO oauth_client_details VALUES('curl_client','product_api', '$2a$10$5ze/vcFOsQBF1og.s.eQ0.8VdsUXh7zzul8VM0Dzcq/NKVNrD8ffO', 'read,write', 'client_credentials', 'http://localhost:7001/oauth2/accessToken', 'ROLE_PRODUCT_ADMIN', 7200, 0, null, 'true');
INSERT INTO oauth_client_details VALUES ('client_code','product_api','$2a$10$5ze/vcFOsQBF1og.s.eQ0.8VdsUXh7zzul8VM0Dzcq/NKVNrD8ffO','read,write','authorization_code,refresh_token','http://localhost:7001/oauth2/code','ROLE_PRODUCT_ADMIN',7200,72000,null,'true');
INSERT INTO oauth_client_details VALUES ('client_implicit', 'product_api' ,'$2a$10$5ze/vcFOsQBF1og.s.eQ0.8VdsUXh7zzul8VM0Dzcq/NKVNrD8ffO', 'read,write' ,'implicit', 'http://localhost:7001/oauth2/accessToken','ROLE_PRODUCT_ADMIN',7200,72000,null,'true');
因为项目使用了BCryptPasswordEncoder加密器,所以数据库的密码统一加密存储。
用户名 | 密码 | 权限 |
oauth_admin | password | ROLE_OAUTH_ADMIN |
resource_admin | password | ROLE_RESOURCE_ADMIN |
product_admin | password | ROLE_PRODUCT_ADMIN |
1、Entity层
package com.scb.oauth2authenticationserver.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import javax.persistence.*;
import java.io.Serializable;
@Data
@Entity
@Table(name = "authority")
public class Authority implements GrantedAuthority, Serializable {
private static final long serialVersionUID = -4737795841774495818L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "AUTHORITY")
private String authority;
}
package com.scb.oauth2authenticationserver.entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
@Data
@Entity
@Table(name = "credentials")
public class Credentials implements Serializable {
private static final long serialVersionUID = -1408491858754963752L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "enabled")
private boolean enabled;
@Column(name = "name",nullable = false)
private String name;
@Column(name = "password",nullable = false)
private String password;
@Version
@Column(name = "version",nullable = false)
private Integer version;
@ManyToMany(fetch = FetchType.EAGER)
private List authorities;
}
Entity层是实体层,映射数据表的字段。其中@Version注解是JPA实现的乐观锁机制。
2、Repository层
package com.scb.oauth2authenticationserver.repository;
import com.scb.oauth2authenticationserver.entity.Credentials;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CredentialsRepository extends JpaRepository {
Credentials findByName(String name);
}
3、配置UserDetailsService
package com.scb.oauth2authenticationserver.service;
import com.scb.oauth2authenticationserver.entity.Credentials;
import com.scb.oauth2authenticationserver.repository.CredentialsRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private CredentialsRepository credentialsRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Credentials credentials = credentialsRepository.findByName(username);
if (credentials == null){
throw new UsernameNotFoundException("User "+username+" cannot be found");
}
User user = new User(credentials.getName(),credentials.getPassword(),credentials.isEnabled(),true,true,true,credentials.getAuthorities());
return user;
}
}
UserDetailsService用于做Spring Security登录认证。关于Spring Security认证流程见:Spring Security 认证流程详解
4、SpringSecurityConfig
package com.scb.oauth2authenticationserver.config;
import com.scb.oauth2authenticationserver.service.JdbcUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return new JdbcUserDetailsService();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**","/js/**","/fonts/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// .addFilter()
// .antMatcher("oauth/authorize")
.authorizeRequests()
.antMatchers("/login","/logout.do").permitAll()
.antMatchers("/**").authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login.do")
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/login")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout.do"))
.and()
.userDetailsService(userDetailsServiceBean());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceBean())
.passwordEncoder(passwordEncoder());
}
}
SpringSecurityConfig模块一共有3个configure,分别是认证相关的AuthenticationManagerBuilder和web相关的WebSecurity、HttpSecurity。
5、Controller层
package com.scb.oauth2authenticationserver.controller;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.approval.Approval;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
public class LoginController {
@Autowired
private JdbcClientDetailsService clientDetailsService;
@Autowired
private ApprovalStore approvalStore;
@Autowired
private TokenStore tokenStore;
@InitBinder
protected void init(HttpServletRequest request, ServletRequestDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
@RequestMapping("/login")
public String loginPage() {
tokenStore.findTokensByClientId("client_code").stream().forEach(accessToken -> log.info(accessToken.toString()));
return "login";
}
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "redirect:/login?logout";
}
@RequestMapping("/")
public ModelAndView root(Map model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info(authentication.getName());
List clientDetails = clientDetailsService.listClientDetails();
clientDetails.stream().forEach(clientDetails1 -> log.info(clientDetails1.toString()));
List approvals = clientDetails.stream()
.map(clientDetails1 -> approvalStore.getApprovals(authentication.getName(), clientDetails1.getClientId()))
.flatMap(Collection::stream)
.collect(Collectors.toList());
approvals.stream().forEach(approval -> log.info(approval.toString()));
model.put("approvals", approvals);
model.put("clientDetails", clientDetails);
return new ModelAndView("index", model);
}
@RequestMapping(value = "/approval/revoke", method = RequestMethod.POST)
public String revokeApproval(@ModelAttribute Approval approval) {
log.info(approval.toString());
Boolean bool = approvalStore.revokeApprovals(Arrays.asList(approval));
log.info(bool.toString());
tokenStore.findTokensByClientIdAndUserName(approval.getClientId(), approval.getUserId())
.forEach(tokenStore::removeAccessToken);
return "redirect:/";
}
}
LoginController定义了如下四个API:
@InitBinder注解用于SpringMVC表单类型转换(比如这里对日期格式做格式化),具体转换在editor层,代码就不列出来了。有关@InitBinder注解的更多知识见:SpringMVC注解@initbinder解决类型转换问题
package com.scb.oauth2authenticationserver.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import lombok.extern.slf4j.Slf4j;
/*
Authorization Server Config
*/
@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("jdbcUserDetailsService")
private UserDetailsService userDetailsService;
// @Autowired
// private AuthenticationManager authenticationManager;
public AuthServerConfig() {
super();
}
/*
oauth_access_token Table
*/
@Bean
public TokenStore tokenStore() {
JdbcTokenStore tokenStore = new JdbcTokenStore(dataSource);
log.info("Create TokenStore :: " + tokenStore);
return tokenStore;
}
/*
oauth_client_details Table
用于配置client信息
*/
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
log.info("Create ClientDetailsService :: " + clientDetailsService);
return clientDetailsService;
}
/*
ApprovalStore:用于保存、检索user approval
oauth_approvals Table
*/
@Bean
public ApprovalStore approvalStore() {
JdbcApprovalStore approvalStore = new JdbcApprovalStore(dataSource);
log.info("Create ApprovalStore :: " + approvalStore);
return approvalStore;
}
/*
oauth_code Table
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
JdbcAuthorizationCodeServices authorizationCodeServices = new JdbcAuthorizationCodeServices(dataSource);
log.info("Create AuthorizationCodeServices :: " + authorizationCodeServices);
return authorizationCodeServices;
}
/*
AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.checkTokenAccess("permitAll()");
}
/*
ClientDetailsServiceConfigurer:用来配置客户端详情服务
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
/*
AuthorizationServerEndpointsConfigurer:来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.approvalStore(approvalStore())
.userDetailsService(userDetailsService)
//.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
}
授权服务器配置:使用 @EnableAuthorizationServer 来配置授权服务机制,并继承 AuthorizationServerConfigurerAdapter 该类重写 configure 方法定义授权服务器策略。
TokenStore总共有四种:
下面在列出OAuth2的一些默认端点:
本文篇幅较长,故先只讲解了oauth2-authentication-server模块。剩下的内容见下一篇文章。
本文是基于springboot+springsecurity+oauth2整合(并用mysql数据库实现持久化客户端数据)的教程上进行二次开发的
下载项目:https://download.csdn.net/download/qq_37771475/12054521