概述
单点登录,英文是 Single Sign On(缩写为 SSO)。就是多个站点公用一台认证服务器,比如下图我刚回答了个问题,在写一篇博客,是不需要再次登录的;而且各站点可以通过该登录状态实现交互。
**注意:登录是一个独立的系统如下图:**不管在系统1还是系统2登录,他们都会去调用认证授权,其目的就是为了保护数据安全性,判断用户的合法性!
快速入门
- 单点登陆系统解决方案设计
本次项目中用到的技术有①JWT ②SpringSecurity安全框架 ③OAuth2
JWT解释
注:JWT中不会存储用户密码,一般存储权限等
- 创建父工程sso 修改pom和配置文件并且在父工程中定义版本
1.父工程maven
<?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>
<groupId>com.jt</groupId>
<artifactId>02-sso</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>sso-system</module>
</modules>
<!--父工程中定义了版本,子工程中则不需要在定义了-->
<dependencyManagement>
<dependencies>
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring CloudAlibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope><!--provided表示只提供编译服务-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope><!--test表示测试类只能写到maven工程的test目录下-->
<!--排除junit4 的测试引擎-->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!--定义统一编译版本-->
<build>
<plugins>
<plugin><!--maven的编译插件-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target><!--运行的时候指定jdk8-->
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2在创建一个子工程sso-system继承父工程
<?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">
<parent>
<artifactId>02-sso</artifactId>
<groupId>com.jt</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sso-system</artifactId>
<dependencies>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--nacos discover-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--spring boot web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
编写配置文件bootstrap.yml
server:
port: 8061
spring:
application:
name: sso-system
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
discovery:
server-addr: localhost:8848
datasource:
url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
logging:
level:
com.jt: debug
然后测试一下数据控连接
创建实体类User
创建mapper实现基于用户名称查询用户信息和基于用户id查询用户权限
1.3统一认证工程auth
统一认证工程的设计及实现
目的:用户登录时,调用此工程对用户身份进行核验,并授权
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">
<parent>
<artifactId>02-sso</artifactId>
<groupId>com.jt</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sso-auth</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringSecurity+JWT+oauth2对登录的用户进行身份校验,颁发令牌-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
</project>
配置文件
server:
port: 8071
spring:
application:
name: sso-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
启动类
启动类启动成功后会在控制台生成一个秘钥,然后访问端口号进去一个SpringSecurity进入一个页面,user为底层创建的用户,秘钥控制台生成,登录以后404则代表登录成功,因为没有要跳转的页面,会出现404异常
定义用户信息处理对象
定义User:用于封装sso-system工程去查询到的用户信息
定义远程service对象,用于实现远程用户信息调用
定义用户登陆业务逻辑处理对象
package com.jt.auth.service.impl;
import com.jt.auth.service.RemoteUserService;
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.stereotype.Service;
import java.util.List;
/**
* 我们的项目在完成用户身份认证时,
* 底层会通过UserDetailsService接口的实现类获取用户信息
* (默认会去内存中,也可以从数据库或者远端服务获取)
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
/**
* 远程服务(sso-system)调用接口
*/
@Autowired
private RemoteUserService remoteUserService;
/**
* 基于用户名获取用户和用户权限信息并封装
* @param username 这个用户名是用户端的输入
* @return User数据和User权限
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.基于用户名获取远端(现在这里指system服务)用户信息
com.jt.auth.pojo.User user = remoteUserService.listUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
//2.基于远端用户id获取用户权限id
List<String> userPermissions =
remoteUserService.listUserByUserIdPermissions(user.getId());
//3.封装用户信息并返回,交给认证管理器(AuthenticationManager)对用户身份进行认证
return new User(username,
user.getPassword(),
AuthorityUtils.createAuthorityList(
userPermissions.toArray(new String[]{})));
//AuthenticationManager这个接口底层已经提供好认证方法,这里只用给他提供数据即可
}
}
定义Security配置类
package com.jt.auth.config;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 因为用户端输入密码以后,要与数据库查询出来的密码进行比对,
* 所以这里要提供一个密码加密对象,对用户端输入的密码进行加密然后在比对
* BCryptPasswordEncoder内置了一种不可逆的加密算法
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* AuthenticationManager主要服务于OAuth2的配置
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
定义Oauth2认证授权配置
package com.jt.auth.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
/**
* OAuth2是一种协议或者说是一种规范,他定义了完成用户身份认证和授权的一种方式
* 业务逻辑:
* 1.认证客户端对象(例如表单,也可以是令牌(QQ或微信等第三方令牌))
* 2.拿到数据要去认证管理服务器
* 3.认证管理服务器要让system服务拿到数据,
* 然后对客户端提交过来的数据和system取数据库拿到的数据进行比较
* 4.认证成功后颁发令牌
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
/**
* 用户端需要携带什么资源进行认证
* @param clients
* @throws Exception
*/
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//把规则写到内存里
.withClient("gateway-client")//定义客户端的标识(客户端到认证服务地址去认证时,需要携带这个信息)
.secret(passwordEncoder.encode("123456"))//定义客户端携带的秘钥
.authorizedGrantTypes("password", "refresh_token")//定义授权的类型,password基于密码进行认证,refresh_token基于刷新令牌认证
.scopes("all");//满足以上条件的客户端都可以认证
}
/**
* 去哪里认证(用户需要携带信息去这个你指定的地址认证)
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")//公开,定义这个认证地址(/oauth/token)
// 用户携带资料到这个地址认证,permitAll()允许所有的到这里认证
.checkTokenAccess("permitAll()")//公开校验token的地址(/oauth/check_token)
.allowFormAuthenticationForClients();//允许form表单的认证方式
}
/**
* 定义认证的对象(谁来帮你完成认证?),认证成功后颁发什么类型的令牌
* @param endpoints
* @throws Exception
*/
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)//指定这个认证管理器对象完成认证
.tokenServices(tokenServices());//定义令牌服务(有默认令牌服务,但默认令牌服务不满足我们的需求)
}
private TokenStore tokenStore;
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
public AuthorizationServerTokenServices tokenServices() {
//1.构建令牌服务对象
DefaultTokenServices tokenServices = new DefaultTokenServices();
//2.设置令牌创建及存储
tokenServices.setTokenStore(tokenStore);
//3.设置令牌增强器(默认是uuid格式的令牌)
tokenServices.setTokenEnhancer(jwtAccessTokenConverter);
//4.设置刷新令牌(服务端要创建一个刷新令牌)
tokenServices.setSupportRefreshToken(true);
//5.设置访问令牌,设置令牌的有效期
tokenServices.setAccessTokenValiditySeconds(3600);
//6.设置刷新令牌
tokenServices.setRefreshTokenValiditySeconds(5400);
return tokenServices;
}
}
但是认证完后需要颁发令牌,默认的不满足我们要求,需要我们手动配置,此类是给上边Oauth2认证授权配置服务的
1.4资源服务工程sso-resource
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">
<parent>
<artifactId>02-sso</artifactId>
<groupId>com.jt</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sso-resource</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--资源服务器添加oauth2依赖的目的不是做认证,而是做授权-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
</project>
配置类
server:
port: 8881
spring:
application:
name: sso-resource
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8180
启动类
配置类
controller
package com.jt.resource.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/resource")
public class ResourceController {
/**
* @PreAuthorize 注解描述的方法为一个授权切入点方法,
* 访问此方法时底层会基于AOP方式进行权限检查,
* 判断用户token的权限中是否包含hasAuthority中指定的权限
* @return
*/
@PreAuthorize("hasAuthority('sys:res:list')")
@GetMapping
public String doSelect() {
return "select resource ok";
}
@PreAuthorize("hasAuthority('sys:res:create')")
@PostMapping
public String doCreate() {
return "create resource ok";
}
@PreAuthorize("hasAuthority('sys:res:update')")
@PutMapping
public String doUpdate() {
return "update resource ok";
}
@PreAuthorize("hasAuthority('sys:res:delete')")
@DeleteMapping
public String doDelete() {
return "delete resource ok";
}
/**
* 登录后访问
* @return
*/
@GetMapping("/export")
public String doExport() {
return "export resource ok";
}
}
定义一个匿名就可以访问资源的controller
package com.jt.resource.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/resource")
public class ResourceController {
/**
* @PreAuthorize 注解描述的方法为一个授权切入点方法,
* 访问此方法时底层会基于AOP方式进行权限检查,
* 判断用户token的权限中是否包含hasAuthority中指定的权限
* @return
*/
@PreAuthorize("hasAuthority('sys:res:list')")
@GetMapping
public String doSelect() {
return "select resource ok";
}
@PreAuthorize("hasAuthority('sys:res:create')")
@PostMapping
public String doCreate() {
return "create resource ok";
}
@PreAuthorize("hasAuthority('sys:res:update')")
@PutMapping
public String doUpdate() {
return "update resource ok";
}
@PreAuthorize("hasAuthority('sys:res:delete')")
@DeleteMapping
public String doDelete() {
return "delete resource ok";
}
/**
* 登录后访问
* @return
*/
@GetMapping("/export")
public String doExport() {
return "export resource ok";
}
}
测试