使用 Spring Cloud 2 + OAuth2 + Security 搭建授权服务
前言:
参考了很多服务搭建教程,但是报乱七八糟的灵异错误,最后一一调试才搞定,心累,记下笔记。oauth2的原理请大家自行google、百度,这里不做赘述。
一、新建项目 oauth2-server ,添加依赖
因为oauth2 jar包中包含spring-cloud-starter-security 所以没有引用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
完整的pom.xml,里面有多余的jar包引用,是我自己以后会用到的,如果不需要可以自行删除
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ms</groupId>
<artifactId>oauth2-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth2-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二、基本的配置和主要代码
启动类
package com.ms.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Oauth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2ServerApplication.class, args);
}
}
主要的config配置
WebSecurityConfig
package com.ms.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 〈security配置〉
* SecurityConfig 用于保护oauth要开放的资源,同时主要作用于client端以及token的认证(Bearer auth)
*/
@Configuration
@EnableWebSecurity
@Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
System.out.println("password " + PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"));
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**").authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = passwordEncoder().encode("123456");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("ADMIN");
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
}
AuthorizationServerConfig
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* 〈OAuth2认证服务器〉
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Primary
@Bean
public TokenStore inMemoryTokenStore() {
return new InMemoryTokenStore();
}
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(inMemoryTokenStore())
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String password = passwordEncoder.encode("123456");
clients.inMemory()
// 配置client_id
.withClient("client")
// 配置client_secret
.secret(password)
// 配置访问token的有效期
.accessTokenValiditySeconds(3600)
// 配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
// 配置redirect_uri,用于授权成功后的跳转
.redirectUris("http://www.baidu.com")
// 配置申请的权限范围
.scopes("all")
// 配置grant_type,表示授权类型
.authorizedGrantTypes("authorization_code", "password", "refresh_token");
}
}
新建UserController
package com.ms.auth.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication.getPrincipal();
}
@GetMapping("/hello")
public String hello(String name) {
return "hello";
}
@GetMapping("/admin/hello")
public String admin(String name) {
return "hello admin";
}
@GetMapping("/user/hello")
public String user(String name) {
return "hello user";
}
}
三、resources配置文件
application.yml
spring:
application:
name: oauth2-server
main:
allow-bean-definition-overriding: true
resources:
static-locations: classpath:/templates/,classpath:/static/
server:
port: 8080
logging:
pattern:
level: DEBUG
level:
org:
springframework:
security: DEBUG
log4j2-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="warn" monitorInterval="30">
<Properties>
<!-- 日志显示模板,显示内容的格式如下 -->
<!-- [21:55:33:047] [INFO] - org.apache.juli.logging.DirectJDKLog.log(DirectJDKLog.java:173) - Initializing Spring embedded WebApplicationContext -->
<!-- <Property name="log_pattern" value="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>-->
<Property name="log_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<!-- 保存日志文件目录 -->
<Property name="file_path" value="${sys:user.home}/logs/oauth2/"/>
<!-- <Property name="file_path" value="/logs"/>-->
<!-- 日志文件的最大容量,超过该值就进行备份 -->
<Property name="file_max_size" value="30MB"/>
<!-- 备份的文件夹名称 如下为:2020-02 -->
<Property name="backup_folder" value="$${date:yyyy-MM}"/>
<!-- 备份文件的后缀,日志文件超过file_max_size会备份到filePattern指定的目录下 -->
<Property name="backup_file_suffix" value="-%d{yyyy-MM-dd}-%i.log"/>
</Properties>
<!--定义appender-->
<appenders>
<!--控制台的输出配置-->
<console name="Console" target="SYSTEM_OUT">
<!-- 设置控制台只输出info及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<!--输出日志的格式-->
<PatternLayout pattern="${log_pattern}"/>
</console>
<!-- 所有的日志信息会打印到此文件中,append=false每次启动程序会自动清空 -->
<!-- <File name="all" fileName="${file_path}/all.log" append="true">
<PatternLayout pattern="${log_pattern}"/>
</File>-->
<!--
该RollingFile存储INFO级别的日志,
默认存储到 fileName 文件中
超过SizeBasedTriggeringPolicy的设定值,则存储到 filePattern 文件中
-->
<RollingFile name="RollingFileInfo" fileName="${file_path}/info.log"
filePattern="${file_path}/${backup_folder}/info${backup_file_suffix}">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<!-- 写入日志文件的模板 -->
<PatternLayout pattern="${log_pattern}"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="${file_max_size}"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,超过该数量,会滚动删除前面的记录 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<RollingFile name="RollingFileWarn" fileName="${file_path}/warn.log"
filePattern="${file_path}/${backup_folder}/warn${backup_file_suffix}">
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${log_pattern}"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="${file_max_size}"/>
</Policies>
</RollingFile>
<RollingFile name="RollingFileError" fileName="${file_path}/error.log" filePattern="${file_path}/${backup_folder}/error${backup_file_suffix}">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${log_pattern}"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="${file_max_size}"/>
</Policies>
</RollingFile>
</appenders>
<!-- 只有定义了logger并使用appender-ref,appender才会生效 -->
<loggers>
<!--过滤掉spring和hibernate的一些无用的debug信息-->
<logger name="org.springframework" level="ERROR"/>
<logger name="org.mybatis" level="ERROR">
<!-- 添加如下设置,控制台会再打印一次 -->
<AppenderRef ref="Console"/>
</logger>
<root level="INFO">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
</configuration>
四、验证
4.1 使用postman验证
密码模式获取http://localhost:8080/oauth/token

4.2 获取当前用户
http://localhost:8080/user/getCurrentUser?access_token=cd6f804a-6b5c-4563-a373-b1428e69ed23

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
没有配置资源文件保护
package com.ms.auth.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 ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().requestMatchers().antMatchers("/user/**");// 配置需要保护的资源路径
}
}
{
"error": "invalid_token",
"error_description": "Invalid access token: b4eb3850-81bd-4561-ad4e-e3969c774fb9"
}
token 过期 ,需要重新获取token
客户端方式请求
浏览器直接访问 get请求
http://localhost:8080/oauth/token?client_secret=123456&username=admin&password=123456&grant_type=password&scope=all&client_id=client
五、问题
5.1
{
"timestamp": "2020-06-29T02:06:02.251+00:00",
"status": 401,
"error": "Unauthorized",
"message": "",
"path": "/oauth/token"
}
是因为没有写 Authorization
需要填写Basic Auth username password


浙公网安备 33010602011771号