👨💻作者简介:全干的java博主
🎟️个人主页:无所谓^_^
ps:点赞是免费的,却可以让写博客的作者开心好几天😎
目录
4.AuthorizationServerConfig认证服务器配置
2.ResourceServerConfig资源服务器配置类
前言
为什么要做这个系统:单点登录(Single Sign-On,简称SSO)是一种用户认证和授权的技术,它允许用户在多个应用系统中使用同一组凭据(用户名和密码)登录,避免了用户需要多次输入凭据的麻烦,提高了用户体验。比如阿里的淘宝和天猫,显然是两个系统,但是你只要登录了淘宝,天猫就不需要登录了。
本文将介绍如何使用Spring Security和OAuth2协议搭建一个简单的单点登录系统。
项目下载
gitee:https://gitee.com/wusupweilgy/springboot-vue.git(点个star呀😎)
一、项目介绍
1.系统架构:
本系统采用OAuth2的授权码和密码模式,主要包括以下几个组件:
- 认证服务器(Authorization Server):负责颁发访问令牌(Access Token),验证客户端身份和用户身份。
- 资源服务器(Resource Server):提供受保护的资源,需要验证访问令牌。
- 客户端(Client):需要访问受保护的资源,需要获取访问令牌。
- 用户(User):系统的最终用户,需要登录并授权客户端访问受保护的资源。
在这贴上JustAuth官方的流程图,方便大家理解
2.直白话 OAuth 2 流程
以上流程理解起来可能有些难度,这儿我们给出一个白话版的流程图
首先引入三个角色:
- 用户A:可以理解成你自己
- 网站B:可以理解成 OSChina
- 第三方C:可以理解成 Github
在我们搭建的系统中,Github就相当于第三方授权系统,也就是认证系统,网站B我们可以先理解成资源服务器,因为用户A要访问网站B的资源,但是网站B说必须Github登录才能访问我的资源。不知道这样说大家能否理解一点点,不理解的话,只能怪我文采不好了😶🌫️。
二、创建工程
1.父工程依赖
这里就不教大家怎么创建工程了,因为我默认大家不是小白了😉,所以就贴上依赖了。一定要注意,创建的工程都是maven工程,不是创建springboot工程。因为创建springboot工程的话idea不能继承,还需要手动改pom.xml文件。
2.认证服务器和资源服务器的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
这里我们要创建两个模块,依赖都是一样的。子工程创建完后,看看自己的子工程pom.xml有没有继承父工程,父工程的pom.xml有没有添加子工程
子工程:
<parent>
<artifactId>security_oauth_parent</artifactId>
<groupId>com.wusuowei</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
父工程:
<modules>
<module>security_oauth_server</module>
<module>security_oauth_resouce</module>
</modules>
三、认证服务器实现
1.启动类和application.yml配置
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OauthServerApplication {
public static void main(String[] args) {
SpringApplication.run(OauthServerApplication.class, args);
}
}
server:
port: 9000
servlet:
context-path: /auth
2.WebSecurityConfig安全配置类
主要配置了Security的认证方式和密码编码器
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean //引入PasswordEncoder
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* password密码模式需要使用此认证管理器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
3.CustomUserDetailsService
用于从数据库或其他数据源中获取用户信息,以便进行身份认证和授权。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
//这里写死的,也可以从数据库查询
@Override
public UserDetails loadUserByUsername(String u) throws UsernameNotFoundException {
return new User("admin", passwordEncoder.encode("1234"),
AuthorityUtils.commaSeparatedStringToAuthorityList("product1"));
}
}
4.AuthorizationServerConfig认证服务器配置
每一行我都进行了注释,当然import和@Autowired没有注释哈😂。这里主要是配置了认证服务器的令牌存储方式切换为 JWT模式。
import com.wusuowei.oauthserver.service.impl.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
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.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
@Configuration
@EnableAuthorizationServer//开启认证服务器功能
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenStore tokenStore;
/** 配置被允许访问此认证服务器的客户端详情信息
* 方式1:内存方式管理
* 方式2:数据库管理
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用内存方式
clients.inMemory()
// 客户端id
.withClient("wj-pc")
// 客户端密码,要加密,不然一直要求登录
.secret(passwordEncoder.encode("wj-secret"))
// 资源id, 如商品资源
.resourceIds("product-server")
// 授权类型, 可同时支持多种授权类型
.authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
// 授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
.scopes("all")
// false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码,
.autoApprove(false)
.redirectUris("http://www.baidu.com/");// 客户端回调地址
}
/**
* 重写父类的方法
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//密码模式需要设置此认证管理器
endpoints.authenticationManager(authenticationManager);
// 刷新令牌获取新令牌时需要
endpoints.userDetailsService(customUserDetailsService);
//设置token存储策略
endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
//授权码管理策略,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
//endpoints.authorizationCodeServices(authorizationCodeServices());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//所有人可以访问/oauth/token_key后面获取公钥,默认拒绝访问
security.tokenKeyAccess("permitAll()");
//认证后可访问/oauth/check_token,默认拒绝访问
security.checkTokenAccess("isAuthenticated()");
}
}
5.TokenConfig(JWT配置类)
要配置签名密钥,因为jwt要靠它进行解析,验证JWT的准确性,是否被篡改。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
// JWT 签名秘钥
private static final String SIGNING_KEY = "wj-key";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
}
四、资源服务器实现
资源服务器的搭建就比认证服务器的搭建简单许多了,但是博客还是要贴很多代码,没办法,主要是为了读者大大们都能成功搭建这个系统,所以就。。。😶
1.启动类和application.yml配置
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
@EnableOAuth2Sso//启用单点登录(SSO)功能
@SpringBootApplication
public class OauthResourceApplication {
public static void main(String[] args) {
SpringApplication.run(OauthResourceApplication.class, args);
}
}
server:
port: 8000
servlet:
context-path: /product
#oauth2的认证 appid appsecret
sso-server-url: http://localhost:9000
security:
oauth2:
client:
client-id: wj-pc
client-secret: wj-secret
#认证服务器接受认证的接口地址,这里的地址和端口要与认证服务器中的配置一致
#user-authorization-uri: ${sso-server-url}/oauth/authorize
#根据授权码去请求令牌的地址
#access-token-uri: ${sso-server-url}/oauth/token
resource:
jwt:
# 从认证服务器中获取JWT的密钥地址,验证token时使用,系统启动时会初始化,不会每次验证都请求
key-uri: ${sso-server-url}/auth/oauth/token_key
2.ResourceServerConfig资源服务器配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
@Configuration
// 标识为资源服务器, 所有发往当前服务的请求,都会去请求头里找token,找不到或验证不通过不允许访问
@EnableResourceServer
//开启方法级别权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//配置当前资源服务器的ID
private static final String RESOURCE_ID = "product-server";
/**当前资源服务器的一些配置, 如资源服务器ID **/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// 配置当前资源服务器的ID, 会在认证服务器验证(客户端表的resources配置了就可以访问这个服务)
resources.resourceId(RESOURCE_ID);
//TODO 可以把配置文件的security配置整个去掉,也要把启动类的@EnableOAuth2Sso注解去掉,不然会报错
// 在本地配置一个TokenConfig,跟认证服务器一样的类,进行本地token校验
//.tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
//不创建session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//资源授权规则
.authorizeRequests().anyRequest().permitAll();
// .antMatchers("/product/**").hasAuthority("product")
//所有的请求对应访问的用户都要有all范围的权限
// .antMatchers("/**").access("#oauth2.hasScope('all')");
}
}
3.统一结果返回类
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark sunlightcs@gmail.com
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R setData(Object data) {
put("data",data);
return this;
}
public R() {
put("code", 200);
put("msg", "success");
}
public static R error() {
return error(500, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}
4.需要授权才能访问的接口
为了后面进行测试,只有携带token令牌才能访问这个接口
import com.wusuowei.oauthresource.utils.R;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class ProductController {
@GetMapping("/list")
@PreAuthorize("hasAuthority('product1')")
public R list(Authentication authentication) {
List<String> list = new ArrayList<>();
list.add("huawei");
list.add("vivo");
list.add("oppo");
System.out.println(authentication);
return R.ok().setData(list);
}
}
五、测试
1.授权码方式
就好比前面打的比方,用户A想访问网站B,但是网站B说必须登录后才允许访问,所以网站B提供了一个GitHub的第三方授权登录链接,用户A点击后,跳转到GitHub的登录页,填写GitHub的账号密码,然后点击授权,GitHub就会返回给网站B一个授权码,网站B再通过这个授权码访问GitHub获取token的接口,获取令牌。优点啰嗦了,哈哈,就是希望大家能理解这个模式,因为微信的扫码登录也是这个模式。
接下来开始测试,发送请求获取授权码code。
访问:http://localhost:9000/auth/oauth/authorize?client_id=wj-pc&response_type=code
这里的client_id是在AuthorizationServerConfig中配置的。
输入账号密码进行登陆,账号和密码:admin/1234 ,是在CustomUserDetailsService中配置的
登陆成功后,选择Approve,点击Authorize,这里跳转到www.baidu.com ,并且后面携带了code,这里的code就是授权码,后面我们就可以通过授权码来获取令牌(access_token)
通过授权码获取令牌
我使用的是Apifox工具测试(用postman也一样):http://localhost:9000/auth/oauth/token,具体的参数配置如下,这里的grant_type是authorization_code,code是上一步获取的code
这里的username和password是在AuthorizationServerConfig中配置的
发送请求后,成功获得了access_token
注意,code只能获取一次access_token,获取后就会失效,第二次获取就会失败
2.密码授权模式
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己在服务提供商(认证服务器)上的用户名和密码,然后客户端通过用户提供的用户名和密码向服务提供商(认证服务器)获取令牌。
如果用户名和密码遗漏,服务提供商(认证服务器)无法判断客户端提交的用户和密码是否盗取来的,那意味着令牌就可随时获取,数据被丢失。
所以密码授权模式适用于产品都是企业内部的,用户名密码共享不要紧。如果是第三方这种不太适合。也适用手机APP提交用户名密码。
访问:localhost:9000/auth/oauth/token
填上请求参数:
发送请求即可获取到access_token:
篇幅有限,这里就介绍这两种常用的模式了,其他模式不常用就不演示了
最后一项测试就是拿着令牌去访问我们
资源服务器的资源了
访问:http://localhost:8000/product/list,请求头参数一定要写对Authorization,值为bearer后面加一个空格,再加上你的access_token令牌
小结
本文介绍了如何使用Spring Security和OAuth2协议搭建一个简单的单点登录系统。。希望本文可以帮助你进一步学习和掌握这两个技术。如果这篇文章有不足的地方希望大家多多指出,如果有幸帮助到你,希望读者大大们可以给作者点个赞呀😶🌫️😶🌫️