SpringSecurity整合OAuth2
本文参考:https://mrbird.cc/Spring-Security-OAuth2-Guide.html
什么是OAuth
OAuth 2.0 的标准是 RFC 6749 文件。
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。
具体概述参考:点击这里
OAuth的四种授权模式
OAuth相关的名词:(以在Chrome上经QQ授权登录B站浏览视频为例)
- Third-party application 第三方应用程序:B站
- HTTP service HTTP服务提供商:QQ(腾讯)
- Resource Owner 资源所有者:QQ登录者
- User Agent 用户代理:Chrome浏览器;
- Authorization server 认证服务器:QQ提供的第三方登录服务;
- Resource server 资源服务器:B站提供的服务,比如高清视频,弹幕发送等(需要认证后才能使用)。
认证服务器和资源服务器可以在同一台服务器上,比如前后端分离的服务后台,它即供认证服务(认证服务器,提供令牌),客户端通过令牌来从后台获取服务(资源服务器);它们也可以不在同一台服务器上,比如上面第三方登录的例子。
授权方式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
授权码模式是最能体现OAuth2协议,最严格,流程最完整的授权模式,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
👇授权流程图
A. 客户端将用户导向认证服务器;
B. 用户决定是否给客户端授权;
C. 同意授权后,认证服务器将用户导向客户端提供的URL,并附上授权码;
D. 客户端通过重定向URL和授权码到认证服务器换取令牌;
E. 校验无误后发放令牌。
其中A步骤,客户端申请认证的URI,包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为”code”,标识授权码模式
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
- grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
- client_id:表示客户端ID,必选项。
密码模式
在密码模式中,用户像客户端提供用户名和密码,客户端通过用户名和密码到认证服务器获取令牌。流程如下所示:
A. 用户向客户端提供用户名和密码;
B. 客户端向认证服务器换取令牌;
C. 发放令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:
- grant_type:表示授权类型,此处的值固定为”password”,必选项。
- username:表示用户名,必选项。
- password:表示用户的密码,必选项。
- scope:表示权限范围,可选项。
隐藏式
凭证式
SpringSecurity OAuth2整合
授权模式
搭建项目运行环境
-
新建SpringBoot项目并配置pom文件
<?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 http://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.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.duck.code</groupId> <artifactId>Springboot-OAuth2</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
创建实体类配置用户信息
@Data public class Coder implements Serializable { private static final long serialVersionUID = 3191927289420949930L; private String password; private String username; // 以下为UserDetails的配置信息 private Set<GrantedAuthority> authorities; // 用户包含的权限 private boolean accountNonExpired = true; // 账户是否过期 private boolean accountNonLocked = true; // 账户是否锁定 private boolean credentialsNonExpired = true; // 凭证是否过期 private boolean enabled = true; // 启用用户进行身份验证 }
创建CoderDetailService
CoderDetailService 实现 UserDetailsService用于用户信息认证
@Service
public class CoderDetailService implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder; // 用于密码加密
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Coder user = new Coder();
user.setUsername(username); // 从前端获取username(输入任意用户名)
user.setPassword(this.passwordEncoder.encode("123123"));
return new User(username,user.getPassword(),user.isEnabled(),
user.isAccountNonExpired(),user.isCredentialsNonExpired(),
user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("user:add"));
}
}
UserDetailsService
public interface UserDetailsService {
// loadUserByUsername)()方法返回 UserDetails
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // 获取用户包含的权限,返回权限集合
// 获取密码和用户名
String getPassword();
String getUsername();
boolean isAccountNonExpired(); // 判断账户是否未过期
boolean isAccountNonLocked(); // 判断账户是否未锁定;
boolean isCredentialsNonExpired(); // 判断用户凭证是否没过期,即密码是否未过期;
boolean isEnabled(); // 判断是否对用户进行身份验证
}
创建Security配置类
@Order(2) // 相比于DuckResourceServerConfig配置文件优先加载SecurityConfig
@EnableWebSecurity
public class DuckSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Spring Security内部实现好的 BCryptPasswordEncoder。
* BCryptPasswordEncoder的特点就是,对于一个相同的密码,每次加密出来的加密串都不同
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/oauth/**","/login/**","/logout/**","/get/code")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.csrf().disable()
.formLogin().permitAll();
}
}
@Order(2)的作用:以下环节有一个请求资源接口,配置访问该接口需要用到ResourceServerConfigurerAdapter接口,其优先级@Order(3)大于WebSecurityConfigurerAdapter导致无法访问到 http://localhost:8080/get/code 获取到授权码,故提前设置!
创建认证服务器配置类
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class DuckAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("code_duck") // 请求方客户端
.secret(passwordEncoder.encode("123123"))
// AUTHORIZATION_CODE,表示采用的授权方式是授权码
// refresh_token刷新获取新的令牌;
.authorizedGrantTypes("password","refresh_token","authorization_code")
// 要求的授权范围:all/read/write,读写,只读,只写
.scopes("all")
// 接受请求后的跳转网址,会携带授权码到该地址
.redirectUris("http://localhost:8080/get/code");
}
}
创建控制器返回授权码
@RestController
public class OAuthController {
@GetMapping(value = "/get/code")
public Map<String, Object> getCode(HttpServletRequest request, HttpServletResponse response){
Map<String, Object> map = new HashMap<>();
map.put("code",request.getParameter("code"));
map.put("state",request.getParameter("state"));
return map;
}
}
访问测试——获取授权码
上面 URL 中,response_type
参数表示要求返回授权码(code
),client_id
参数让 B 知道是谁在请求,redirect_uri
参数是 B 接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围。
🍀输入任意用户名,密码123123
🍁跳转页面进行授权
🌳授权成功后返回授权码code
获取token
🌴使用postman发送post请求:http://localhost:8080/oauth/token?grant_type=authorization_code&code=nW8dj5&client_id=code_duck&redirect_uri=http://localhost:8080/get/code&scope=all
设置header请求头信息 Authorization:Basic (client_id:client_secret经过base64加密后的值)
加密地址:http://tool.chinaz.com/Tools/Base64.aspx
设置请求参数:
一个授权码只能授权一个token,多次请求无效
通过token获取资源服务器资源
-
配置DuckResourceServerConfig
@Configuration @EnableResourceServer // 配置资源服务器,让客户端可以通过合法的令牌来获取资源。 public class DuckResourceServerConfig extends ResourceServerConfigurerAdapter { }
-
创建资源接口(该接口当授权用户访问时,无需进行认证)
@GetMapping(value = "/get/info") public String getInfo(){ return "code duck!"; }
-
请求上方地址并设置请求头Authorization
密码模式
对授权模式中的配置类进行修改
DuckSecurityConfig中注入AuthenticationManager
/**
* 密码模式需要用到 AuthenticationManager
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
修改认证服务器DuckAuthorizationServerConfigurer
@Resource
private AuthenticationManager authenticationManager;
@Resource
private CoderDetailService coderDetailService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager).
userDetailsService(coderDetailService);
}
重启访问获取token
访问资源服务器
携带 token即设置请求头Authorization :Bearer token 访问http://localhost:8080/get/info
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步