admin管理员组文章数量:1568307
1.单点登陆系统概述
单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点
。而且,各站点间可以通过该登录状态直接交互
。
2.单点登陆系统解决方案设计
2.1解决方案一:用户登陆成功以后,将用户登陆状态存储到redis数据库
在这套方案中,用户登录成功后**,会基于UUID生成一个token**,然后与用户信息绑定在一起存储到数据库.后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般。
2.2解决方案2:用户登陆成功以后,将用户信息存储到token(令牌),然后写到客户端进行存储
在这套方案中,用户登录成功后,会基于JWT技术生成一个token,用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库。
3.单点登陆系统初步设计
3.1服务设计
服务基于业务划分
,系统(system)服务只提供基础数据(例如用户信息、日志信息等),认证服务(auth)负责完成用户身份校验、密码比对,资源服务(resource)代表一些业务服务(例如我的订单、收藏等)。
3.2创建项目工程
父工程:02-sso
配置pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0"
xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jt</groupId>
<artifactId>02-sso</artifactId>
<version>1.0-SNAPSHOT</version>
<!--maven父工程的pom文件中一般要定义子模块,
子工程中所需依赖版本的管理,公共依赖并且父工程的打包方式一般为pom方式-->
<!--第一步: 定义子工程中核心依赖的版本管理(注意,只是版本管理)-->
<dependencyManagement>
<dependencies>
<!--spring boot 核心依赖版本定义(spring官方定义)-->
<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 微服务规范(由spring官方定义)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type><!--假如scope是import,type必须为pom-->
<scope>import</scope><!--引入三方依赖的版本设计-->
</dependency>
<!--Spring Cloud alibaba 依赖版本管理 (参考官方说明)-->
<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>
<!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
<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表示只能在test目录下使用此依赖-->
<exclusions>
<exclusion><!--排除一些不需要的依赖-->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--其它依赖...-->
</dependencies>
<!--第三步: 定义当前工程模块及子工程的的统一编译和运行版本-->
<build><!--项目构建配置,我们基于maven完成项目的编译,测试,打包等操作,
都是基于pom.xml完成这一列的操作,但是编译和打包的配置都是要写到build元素
内的,而具体的编译和打包配置,又需要plugin去实现,plugin元素不是必须的,maven
有默认的plugin配置,常用插件可去本地库进行查看-->
<plugins>
<!--通过maven-compiler-plugin插件设置项目
的统一的jdk编译和运行版本-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!--假如本地库没有这个版本,这里会出现红色字体错误-->
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.3系统基础服务工程设计及实现
本次设计系统服务(System),主要用于提供基础数据服务,例如日志信息,用户信息等
3.3.1数据库表设计
3.3.2创建子工程sso-system
3.3.2.1添加依赖:
<!--1.数据库访问相关-->
<!--1.1 mysql 数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--1.2 mybatis plus 插件-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</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>
<!--Web 服务相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.3.2.2在项目中添加bootstrap.yml文件
server:
port: 8061
spring:
application:
name: sso-system
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
datasource:
url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: 123456
mybatis-plus:
type-aliases-package: com.jt.system.pojo
#将所有的映射文件全部加载
mapper-locations: classpath:/mappers/*.xml
#开启驼峰映射
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.jt: debug
可将连接数据库的配置,添加到配置中心
3.3.2.3在项目中添加启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class,args);
}
}
3.3.2.4在项目中添加单元测试类,测试数据库连接
package com.jt;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
public class DataSourceTests {
@Autowired
private DataSource dataSource;//HikariDataSource
@Test
void testGetConnection() throws SQLException {
Connection conn=
dataSource.getConnection();
System.out.println(conn);
}
}
3.4POJO对象逻辑实现
添加项目User对象,用于封装用户信息
java中所有用于存储数据的对象,都建议实现序列化接口,并且添加一个序列化id
可参考:String,Integer,ArrayList,HashMap
@Data //编译时有效
@Accessors(chain = true)
//@TableName("tb_users"),假如sql语句自己写,不需要此注解指定表名
public class User implements Serializable {//此接口起标识性作用
//序列化id
private static final long serialVersionUID = -9218088594214708448L;
private Long id;
private String username;
private String password;
private String status;
}
实现序列化id,alt+enter自动添加UID
Service中@Autowired注入UserMapper中对象红色波浪线解决
Java中连接池设计需要遵循的数据源规范是(javax.sql.DataSource),连接池这块能想到的设计模式有(单例,享元,桥接,slf4j门面)
3.5创建UserMapper接口,并定义基于用户名查询用户信息,基于用户id查询用户权限信息的方法
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select id,username,password,status from tb_users where username=#{username}")
User selectUserByUsername(String username);
@Select("select distinct m.permission " +
"from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id " +
"join tb_menus m on rm.menu_id=m.id " +
"where ur.user_id=#{userId}")
List<String> selectUserPermissions(Long userId);
}
3.6创建UserMapperTests类,对业务方法做单元测试
@SpringBootTest
public class UserMapperTests {
@Autowired
private UserMapper userMapper;
@Test
void testSelectUserByUsername(){
User user = userMapper.selectUserByUsername("admin");
System.out.println(user);
//断言测试,测试结果不正确,就抛异常
//Assert.notNull(user, "user is not exist");
//推荐此种方式
Assertions.assertNotNull(user,"user is not null");
}
@Test
void testSelectUserPermissions(){
List<String> permissions = userMapper.selectUserPermissions(1L);
System.out.println(permissions);
Assertions.assertNotNull(permissions,"permissions is not null");
}
}
3.7定义service接口
public interface UserService {
User selectUserByUsername(String username);
List<String> selectUserPermissions(Long userId);
}
3.8定义service接口实现类
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User selectUserByUsername(String username) {
//Assert.notNull(username,"username can not be null");
//判断是否为空,抛异常
if (StringUtils.isEmpty(username)){
throw new IllegalArgumentException("username can not be null");
}
return userMapper.selectUserByUsername(username);
}
/**
* 此注解描述的方法为缓存切入点方法,从数据库查询到数据后,
* 可以将数据存储到本地的一个缓存对象中(底层是一个map对象)
* */
@Cacheable(value = "permissionCache")
@Override
public List<String> selectUserPermissions(Long userId) {
return userMapper.selectUserPermissions(userId);
}
}
@EnableCaching //开启spring中的缓存机制,扫描哪个类中用到了缓存
@Cacheable(value = "permissionCache"),此注解描述的方法为缓存切入点方法,从数据库查询到数据后, 可以将数据存储到本地的一个缓存对象中(底层是一个map对象)
3.9Controller对象逻辑实现
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/login/{username}")
public User doSelectUserByUsername(@PathVariable String username){
return userService.selectUserByUsername(username);
}
@GetMapping("/permission/{userId}")
public List<String> doSelectUserPermissions(@PathVariable Long userId){
return userService.selectUserPermissions(userId);
}
}
4.统一认证工程设计及实现
用户登陆时调用此工程对用户身份进行统一身份认证和授权。
4.1创建工程sso-auth及初始化
4.1.1编辑pom文件
<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>
<!--SSO技术方案:SpringSecurity+JWT+oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
4.1.2在sso-auth工程中创建bootstrap.yml文件
server:
port: 8071
spring:
application:
name: sso-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
4.1.3添加项目启动类
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
4.1.4启动并访问项目
项目启动时,系统会默认生成一个登陆密码
打开浏览器输入http://localhost:8071呈现登录页面
默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登录页面,所以会出现404)。
4.2定义用户信息处理对象
第一步:定义User对象,用于封装从数据库查询到的用户信息
package com.jt.auth.pojo;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private static final long serialVersionUID = 4831304712151465443L;
private Long id;
private String username;
private String password;
private String status;
}
第二步:定义用户远程调用Feign接口RemoteUserService ,基于此接口调用sso-system服务中的用户信息
@FeignClient(value = "sso-system",
contextId = "remoteUserService")
public interface RemoteUserService {
@GetMapping("/user/login/{username}")
User selectUserByUsername(@PathVariable(name = "username") String username);
/**
* 基于用户id查权限
* */
@GetMapping("/user/permission/{userId}")
List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}
第三步:定义远程Service对象,用于实现远程用户信息调用
/**
* 构建UserDetailsService实现类,在此类基于RemoteUserService接口进行远程服务调用,
* 调用sso-system服务,
*/
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.基于用户名获取用户信息(远程Feign方式服务调用)
com.jt.auth.pojo.User user = remoteUserService.selectUserByUsername(username);
if(user == null)
throw new UsernameNotFoundException("user id not exist");
//2.基于用户id获取用户权限信息
List<String> permissions = remoteUserService.selectUserPermissions(user.getId());
log.debug("permissions {}",permissions);
//3.封装用户信息并返回
User userDetails = new User(username,
user.getPassword(),
AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
return userDetails; //交给spring security的认证中心,进行认证分析(比对)
}
}
5.定义Security配置类
在此类中配置认证规则
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 初始化加密对象
* 此对象提供了一种不可逆的加密方式,相对于md5方式会更加安全
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**配置认证规则
* 此方法为http请求方法,可配置:
* 哪些资源放行(不用登录即可访问),不做任何配置,默认所有资源都可匿名访问
* 哪些资源必须认证(登录)才能访问
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
//禁用跨域攻击,假如没有禁用,使用postman,httpclient这些工具登录失败403
//http.csrf().disable();
//所有**资源必须认证才能访问,403没有权限
//http.authorizeRequests().antMatchers("/**").authenticated();
//放行所有的资源(在资源服务中授权)
//http.authorizeRequests().anyRequest().permitAll();
//配置需要认证的,例如default.html,其它的都放行
/* http.authorizeRequests()
.antMatchers("/default.html")
.authenticated()
.anyRequest().permitAll();*/
//登录配置,去哪里认证,认证成功或失败的处理器是谁
// http.formLogin().defaultSuccessUrl("index.html"); //redirect:index.html重定向
// http.formLogin().successForwardUrl("/doIndex");
//前后端分离的写法,登录成功要返回json字符串
http.formLogin()
.successHandler(successHandler())
.failureHandler(failureHandler());
}
//定义认证成功处理器
//登录成功以后返回json数据
@Bean
public AuthenticationSuccessHandler successHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("status", 200);
map.put("message", "login success");
writeJsonToClient(response,map);
}
};
}
//lambda
public AuthenticationFailureHandler failureHandler() {
return (request, response, authentication) ->{
Map<String,Object> map = new HashMap<>();
map.put("status", 201);
map.put("message", "login failure");
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(HttpServletResponse response,Map<String,Object> map) throws IOException {
//将map对象,转换为json
String jsonStr =new ObjectMapper().writeValueAsString(map);
//设置响应数据的编码方式
response.setCharacterEncoding("utf-8");
//设置响应数据的类型
response.setContentType("application/json;charset:utf8");
//将数据响应到客户端
PrintWriter out=response.getWriter();
out.println(jsonStr);
out.flush();
}
}
基于浏览器进行访问测试
启动sso-system,sso-auth服务,然后基于浏览器访问网关,执行登录测试,admin,123456
6.Security 认证流程分析
目前的登陆操作,也就是用户的认证操作,其实现主要基于Spring Security框架,其认证简易流程如下
6.1定义Oauth2认证授权配置
Oauth2是一种协议或规范,定义了完成用户身份认证和授权的方式,
比如:基于密码身份认证,基于指纹,基于第三方令牌认证(QQ,微信登录),但具体完成过程需要一组对象,这些对象构成,有如下几个部分:
0.系统数据资源(类似数据库,文件系统)
1.资源服务器(负责访问资源,例如:商品,订单,库存,会员…)
2.认证服务器(负责完成用户身份的认证)
3.客户端对象(表单,令牌,)
4.资源拥有者(用户)在Oauth2这中规范下,如何对用户什么进行认证?
1.认证的地址(让用户去哪里认证)
2.用户需要携带什么信息去认证(办理)
3.具体完成认证的对象是谁
在Security配置类添加以下方法,在Oauth2Config注入此对象:
//方法返回的对象为后续的oauth2的配置提供服务
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
定义Oauth2Config配置类:
/**
* Oauth2是一种协议或规范,定义了完成用户身份认证和授权的方式,
* 比如:基于密码身份认证,基于指纹,基于第三方令牌认证(QQ,微信登录),
* 但具体完成过程需要一组对象,这些对象构成,有如下几个部分:
* 0.系统数据资源(类似数据库,文件系统)
* 1.资源服务器(负责访问资源,例如:商品,订单,库存,会员...)
* 2.认证服务器(负责完成用户身份的认证)
* 3.客户端对象(表单,令牌,)
* 4.资源拥有者(用户)
*
* 在Oauth2这中规范下,如何对用户什么进行认证?
* 1.认证的地址(让用户去哪里认证)
* 2.用户需要携带什么信息去认证(办理)
* 3.具体完成认证的对象是谁
*/
@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....)
/**
* 在此方法中公开认证地址
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//super.configure(security);
//公开认证地址(/auth/token)
security.tokenKeyAccess("permitAll()")
//公开检查令牌的入口(/oauth/check_token)
.checkTokenAccess("permitAll()")
//允许通过表单方式进行认证
.allowFormAuthenticationForClients();
}
/**
* 定义用户去认证时,需要携带什么信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//super.configure(clients);
clients.inMemory()
//客户端标识
.withClient("gateway-client")
//客户端携带密钥 123456
.secret(passwordEncoder.encode("123456"))
//定义认证类型(允许对哪些数据进行认证)
.authorizedGrantTypes("password","refresh_token")
//作用域,满足如上条件的所有客户端可以来这里进行认证
.scopes("all");
}
/**
* 定义由谁完成认证,认证成功后以怎样形式颁发令牌
* 默认是UUID方式
* */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//super.configure(endpoints);
//定义由谁完成认证
endpoints.authenticationManager(authenticationManager);
//定义用户状态信息的存储(存储到内存,mysql,redis,jwt)
//endpoints.tokenStore(tokenStore());
//设置令牌增强
//endpoints.tokenEnhancer(jwtAccessTokenConverter());
//定义令牌业务存储对象(自己指定令牌规则)
endpoints.tokenServices(tokenServices());
//允许客户端认证的请求方式(默认只支持post)
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
//设置令牌存储对象
tokenServices.setTokenStore(tokenStore);
//进行令牌增强(普通令牌改位jwt令牌)
tokenServices.setTokenEnhancer(jwtAccessTokenConverter);
//设置访问令牌有效期(访问令牌激素hi访问资源时要携带的信息)
tokenServices.setAccessTokenValiditySeconds(3600); //
//设置刷新令牌,在范围跟令牌即将到期时,还可以使用刷新令牌再去请求一个jwt令牌
tokenServices.setSupportRefreshToken(true);
//设置刷新令牌有效期
tokenServices.setRefreshTokenValiditySeconds(7200);
return tokenServices;
}
}
6.2构建令牌生成及配置对象
本次我们借助JWT(Json Web Token-是一种json格式
)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源。
@Configuration
public class TokenConfig {
/**
* 创建令牌信息存储对象
* */
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置签名key,对Jwt令牌进行签名时使用,key不能让客户端知道
jwtAccessTokenConverter.setSigningKey(signingKey);
return jwtAccessTokenConverter;
}
/**签名key,对Jwt令牌签名时使用,这个值可以自己随意定义,*/
private String signingKey="auth";
}
6.3启动postman进行访问测试
检查token信息:
7.资源服务工程设计及实现
资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,都可以方法,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。
7.1业务设计架构
用户访问资源时的认证,授权流程设计如下:
7.2创建sso-resource工程
7.2.1第一步:初始化pom文件依赖
<dependencies>
<!--spring boot web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos discovery-->
<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>
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--在资源服务器添加此依赖,只做授权,不做认证,添加完此依赖以后,
在项目中我们要做哪些事情?对受限访问的资源可以先判断是否登录了,
已经认证用户还要判断是否有权限?
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
7.2.2第二步:创建bootstrap.yml配置文件:
server:
port: 8881
spring:
application:
name: sso-resource
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
7.2.3第三步:创建启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class,args);
}
}
7.2.4第四步:创建资源Controller对象
@RestController
@RequestMapping("/resource")
public class ResourceController {
/**
* 查询资源
* @return
*/
@PreAuthorize("hasAuthority('sys:res:list')")//描述的是一个切入点方法,访问此方法,需加此权限
@GetMapping
public String doSelect(){
return "Select Resource ok";
}
/**
* 创建资源
* @return
*/
@PreAuthorize("hasAuthority('sys:res:create')")
@PostMapping
public String doCreate(){
return "Create Resource OK";
}
/**
* 修改资源
* @return
*/
@PreAuthorize("hasAuthority('sys:res:update')")
@PutMapping
public String doUpdate(){
return "Update Resource OK";
}
/**
* 删除资源
* @return
*/
@DeleteMapping
public String doDelete(){
return "Delete resource ok";
}
}
7.2.5第五步:配置资源认证授权规则ResourceConfig
/**
* 思考?对于一个系统而言,它资源的访问权限你是如何进行分类设计的
* 1)不需要登录就可以访问(例如12306查票)
* 2)登录以后才能访问(例如12306的购票)
* 3)登录以后没有权限也不能访问(例如会员等级不够不让执行一些相关操作)
*/
@Configuration
@EnableResourceServer //启动资源服务器的默认配置
//启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
//super.configure(http);
//1.关闭跨域攻击
http.csrf().disable();
//2.放行相关请求
http.authorizeRequests()
//配置炫耀认证的资源
.antMatchers("/resource/**")
.authenticated()
//除了需要认证的资源,其他资源全部放行
.anyRequest().permitAll();
}
}
7.2.6第六步:配置令牌解析器对象TokenStore
/**
* 在此配置类中配置令牌的生成,存储策略,验签方式(令牌合法性)。
*/
@Configuration
public class TokenConfig {
/**
* 配置令牌的存储策略,对于oauth2规范中提供了这样的几种策略
* 1)JdbcTokenStore(这里是要将token存储到关系型数据库)
* 2)RedisTokenStore(这是要将token存储到redis数据库-key/value)
* 3)JwtTokenStore(这里是将产生的token信息存储客户端,并且token
* 中可以以自包含的形式存储一些用户信息)
* 4)....
*/
@Bean
public TokenStore tokenStore(){
//这里采用JWT方式生成和存储令牌信息
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置令牌的创建及验签方式
* 基于此对象创建的令牌信息会封装到OAuth2AccessToken类型的对象中
* 然后再存储到TokenStore对象,外界需要时,会从tokenStore进行获取。
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=
new JwtAccessTokenConverter();
//JWT令牌构成:header(签名算法,令牌类型),payload(数据部分),Signing(签名)
//这里的签名可以简单理解为加密,加密时会使用header中算法以及我们自己提供的密钥,
//这里加密的目的是为了防止令牌被篡改。(这里密钥要保管好,要存储在服务端)
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//设置密钥
return jwtAccessTokenConverter;
}
/**
* JWT 令牌签名时使用的密钥(可以理解为盐值加密中的盐)
* 1)生成的令牌需要这个密钥进行签名
* 2)获取的令牌需要使用这个密钥进行验签(校验令牌合法性,是否被篡改过)
*/
private static final String SIGNING_KEY="auth";
}
7.2.7第七步:启动Postman进行访问测试:
不携带令牌访问:
携带令牌访问:
没有访问权限:
8.网关工程设计及实现
8.1配置pom文件
<!--网关项目依赖-->
<dependencies>
<!--网关服务依赖(底层netty技术+webflux技术),不能再有web依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</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>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
</dependencies>
8.2创建bootstrap.yml配置文件并进行路由定义
#port
#spring.application.name
#nacos discovery and config
#sentinel
#gateway
#谓词逻辑返回值都为true则执行过滤器
server:
port: 9000
spring:
application:
name: sso-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
sentinel:
transport:
dashboard: localhost:8180
eager: true
# gateway:
# routes:
# - id: router01 #资源服务器路由
# uri: lb://sso-resource #lb表示负载均衡,sso-resource为服务名
# predicates: #谓词对象,可以定义多个谓词逻辑,所有谓词逻辑返回值为true才会去执行filters
# - Path=/sso/resource/**
# filters: #过滤器,是谓词逻辑的下一个执行步骤
# - StripPrefix=1 #去掉path中的第一层目录
# - id: router02 #认证服务器路由
# uri: lb://sso-auth #认证服务
# predicates:
# - Path=/sso/oauth/**
# filters:
# - StripPrefix=1
# globalcors: #跨域配置(写到配置文件的好处是可以将其配置写到配置中心)
# corsConfigurations: #所有跨域配置只是针对ajax请求,因为ajax请求不支持跨域
# '[/**]':
# allowedOrigins: "*"
# allowedHeaders: "*"
# allowedMethods: "*"
# allowCredentials: true
8.3定义启动类
8.4HttpClient访问测试
9.客户端UI工程设计及实现
9.1编辑pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
9.2创建启动类
9.3创建UI工程登陆页面
第一步:在resource目录下创建static目录
第二步:在static目录下创建登陆页面login.html
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<!--ElementUI CSS-->
<link rel="stylesheet" href="https://unpkg/element-ui/lib/theme-chalk/index.css">
<title>login</title>
</head>
<body>
<div class="container" id="app">
<h3>Please Login</h3>
<el-form :model="login1" label-width="80px">
<el-form-item label="用户名">
<el-input v-model="login1.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="login1.password" type="password" placeholder="请输入密码">
</el-input>
</el-form-item>
<el-row>
<el-button type="primary" round @click="doLogin()">主要按钮</el-button>
</el-row>
</el-form>
<!-- <form>
<div class="mb-3">
<label for="usernameId" class="form-label">Username</label>
<input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="passwordId" class="form-label">Password</label>
<input type="password" v-model="password" class="form-control" id="passwordId">
</div>
<button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
</form>-->
</div>
<!--import Vue before Element-->
<script src="https://cdn.jsdelivr/npm/vue@2/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg/element-ui/lib/index.js"></script>
<script src="https://cdn.jsdelivr/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script src="https://unpkg/axios/dist/axios.min.js"></script>
<script>
var vm = new Vue({
el: "#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
data() { //此对象中定义页面上要操作的数据
return {
login1: {
username: "",
password: ""
}
}
},
methods: {//此位置定义所有业务事件处理函数
doLogin() {
//1.定义url
let url = "http://localhost:9000/sso/oauth/token"
//2.定义参数
let params = new URLSearchParams()
params.append('username', this.login1.username);
params.append('password', this.login1.password);
params.append('client_id', "gateway-client");
params.append('client_secret', "123456");
params.append('grant_type', "password");
//3.发送异步请求
axios.post(url, params)
.then((response) => {//ok
alert("login ok")
let result = response.data;
console.log("result", result);
//将返回的访问令牌存储到浏览器本地对象中
localStorage.setItem("accessToken", result.access_token);
location.href = "/resource.html";
//启动一个定时器,一个小时以后,向认证中心发送刷新令牌
/* setTimeout(function () {
})*/
})
.catch((e) => {
console.log(e);
})
}
}
});
</script>
</body>
</html>
9.4创建资源展现页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--ElementUI CSS-->
<link rel="stylesheet" href="https://unpkg/element-ui/lib/theme-chalk/index.css">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>The Resource Page</h1>
<el-button type="primary" plain @click="doSelect">查询我的资源</el-button>
<el-button type="primary" plain @click="doUpdate">修改我的资源</el-button>
<!-- <el-button :plain="true" @click="doSelect">查询我的资源</el-button>
<el-button :plain="true" @click="doUpdate">修改我的资源</el-button>-->
<!-- <button οnclick="doSelect()">查询我的资源</button>
<button οnclick="doUpdate()">修改我的资源</button>-->
</div>
<!--import Vue before Element-->
<script src="https://cdn.jsdelivr/npm/vue@2/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg/element-ui/lib/index.js"></script>
<script src="https://unpkg/axios/dist/axios.min.js"></script>
<script>
new Vue({
el: "#app",
data() {
return{
}
},
methods: {
doSelect() {
let url = "http://localhost:9000/sso/resource";
//获取登录后,存储到浏览器客户端的访问令牌
let token = localStorage.getItem("accessToken");
//发送请求时,携带访问令牌
axios.get(url, {headers: {"Authorization": "Bearer " + token}})
.then(response => {
this.$message({
showClose: true,
message: '恭喜你,查询成功',
type: 'success'
})
console.log(response.data);
})
.catch(function (e) {//失败时执行catch代码块
if (e.response.status == 401) {
this.$message({
showClose: true,
message: '请先登录',
type: 'warning'
})
location.href = "/login.html";
} else if (e.response.status == 403) {
this.$message({
showClose: true,
message: '您没有权限',
type: 'warning'
})
}
console.log("error", e);
})
},
doUpdate() {
let url = "http://localhost:9000/sso/resource";
//获取登录后,存储到浏览器客户端的访问令牌
let token = localStorage.getItem("accessToken");
console.log("token", token);
//发送请求时,携带访问令牌
axios.put(url, "", {headers: {"Authorization": "Bearer " + token}})
.then(response => {
this.$message({
showClose: true,
message: '恭喜你,更新成功',
type: 'success'
})
console.log(response.data);
})
.catch(e => {//失败时执行catch代码块
console.log(e);
if (e.response.status == 401) {
this.$message({
showClose: true,
message: '请先登录',
type: 'warning'
})
location.href = "/login.html";
} else if (e.response.status == 403) {
this.$message({
showClose: true,
message: '您没有权限',
type: 'warning'
})
}
console.log("error", e);
})
}
}
})
</script>
</body>
</html>
9.5打开浏览器进行访问测试
admin 123456
登录后跳转
10.技术摘要应用实践说明
10.2Spring Security 技术
Spring Security 是一个企业级安全框架,由spring官方推出,它对软件系统中的认证,授权,加密等功能进行封装,并在springboot技术推出以后,配置方面做了很大的简化。Spring Security 在企业中实现认证和授权业务时,底层构建了大量的过滤器,如图所示:
绿色部分为认证过滤器,黄色部分为授权过滤器。Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作。
10.3Jwt数据规范
官方JWT规范定义,它构成有三部分,分别为Header(头部),Payload(负载),Signature(签名):
10.3.1Header部分
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将这个 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
10.3.2Payload部分
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。
10.3.3Signature部分
Signature 部分是对前两部分的签名,其目的是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
10.4Oauth2规范
oauth2定义了一种认证授权协议,一种规范,此规范中定义了四种类型的角色:
1)资源有者(User)
2)认证授权服务器(jt-auth)
3)资源服务器(jt-resource)
4)客户端应用(jt-ui)
同时,在这种协议中规定了认证授权时的几种模式:
1)密码模式 (基于用户名和密码进行认证)
2)授权码模式(就是我们说的三方认证:QQ,微信,微博,。。。。)
11.SSO微服务工程中用户行为日志的记录
11.1系统需求分析
用户在sso-resource工程访问我们的资源数据时,获取用户的行为日志信息,然后传递给sso-system工程,将日志信息存储到数据库
11.2系统服务中的日志存储设计
系统服务负责将其它服务获取的用户行为日志写入到数据库。
11.2编辑pojo对象
/**
* 基于此对象封装用户行为日志?
* 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
*/
@Data
@TableName("tb_logs")
public class Log implements Serializable {
private static final long serialVersionUID = 3054471551801044482L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
@TableField("createdTime")
private Date createdTime;
private Integer status;
private String error;
}
11.2.2编辑mapper层
/**
* 用户行为日志的数据访问逻辑对象(DAO),
* 通过此对象完成用户行为日志的持久化操作
* */
@Mapper
public interface LogMapper extends BaseMapper<Log> {
}
11.2.3Service逻辑实现
public interface LogService {
void insertLog(Log log);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
/**
*写日志逻辑,并且希望这个写日志的动作通过一个异步线程去执行
* (因为用户不关心底层日志记录),这个线程不占用web服务(tomcat)的线程资源
* @Async 注解描述方法时,此方法为一个异步切入点方法,
* 此方法会有一个spring内置的线程池中的线程调用执行,
* 注意: 描述的方法不能有返回值
* 需要通过@EnableAsync注解对项目的启动类或者配置类进行描述,启动异步机制
*/
@Async
@Override
public void insertLog(Log log) {
logMapper.insert(log);
}
}
11.2.4Controller 逻辑实现
@RestController
@RequestMapping("/log")
public class LogController {
@Autowired
private LogService logService;
@PostMapping
public void doInsertLog(@RequestBody Log log){
logService.insertLog(log);
}
}
11.2.5HttpClient测试
11.3资源服务中行为日志操作设计
在不修改目标业务方法代码实现的基础之上,访问目标方法时,获取用户行为日志
11.3.1编辑pojo对象
/**
* 基于此对象封装用户行为日志?
* 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
*/
@Data
public class Log implements Serializable {
private static final long serialVersionUID = 8687158282896650331L;
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
private Date createdTime;
private Integer status;
private String error;
}
11.3.2切入点注解定义
构建一个自定义注解,名字为RequiredLog,后续会基于此注解描述作为切入点,定义切入点方法
/**
* 希望由此注解描述的方法为日志切入点记录
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiredLog {
String value() default "";
}
11.3.3在ResourceController使用自定义注解
查询资源:@RequiredLog 注解描述的方法为一个日志切入点方法,访问此方法进行日志写入
/**
* 查询资源
* @RequiredLog 注解描述得方法为一个日志切入点方法,访问此方法进行日志写入
*/
@RequiredLog("查询资源") //自定义注解
@PreAuthorize("hasAuthority('sys:res:list')")//描述的是一个切入点方法,访问此方法,需加此权限
@GetMapping
public String doSelect(){
return "Select Resource ok";
}
11.3.4AOP方式获取并记录日志
需要在bootstrap.yml文件加日志:
定义一个日志切面,基于此切面中的通知方法实现用户行为日志的获取和记录
/**
* 自定义用户行为日志切面
*/
@Aspect
@Component
public class LogAspect {
/**
* 切入点,采用注解方式的切入点表达式,
* 使用RequiredLog注解描述方法时,被描述的方法就是切入点方法
* doLog方法,启动了承载@Pointcut注解的作用
*/
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
public void doLog() {
}
/**
* 通知方法,在此方法内,可以手动调用目标方法执行链,在执行链执行过程中获取用户行为日志,进行封装和记录,
* ProceedingJoinPoint为连接点对象这个类型对象只能应用在@Around注解描述的方法上,并且可以通过此连接点对象
* 获取目标方法信息,调用目标方法执行链,目标方法(切入点方法)执行结果
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
int status = 1;//状态码
String error = null; //错误信息
long time = 0l;//执行时长
//获取目标方法开始执行时间
long t1 = System.currentTimeMillis();
System.out.println("Before:" + t1);
try {
//调用目标方法执行链,这个执行链的终端是切入点方法
Object result = jp.proceed();
//获取目标方法执行结束时间
long t2 = System.currentTimeMillis();
System.out.println("After:" + t2);
time = t2-t1;
return result;
} catch (Throwable e) {
long t3 = System.currentTimeMillis();
System.out.println("Exception:" + t3);
time=t3-t1;
status=0;
error=e.getMessage();
throw e;
}finally {
//无论目标方法是否执行成功,都要记录日志
saveLog(jp,time,status,error);
}
}
//存储用户行为日志
private void saveLog(ProceedingJoinPoint jp,long time,int status,String error) throws Throwable {
//1.获取用户行为日志
//1.1获取目标对象类型(切入点方法所在类的类型)
Class<?> targetClass = jp.getTarget().getClass();
//1.2获取目标方法(切入点方法)
//1.2.1获取方法签名(包含方法信息)
//通过连接点对象获取方法签名对象(此对象包含目标方法的信息)
MethodSignature signature = (MethodSignature)jp.getSignature();
//1.2.2获取方法对象
Method targetMethod = targetClass.getDeclaredMethod(signature.getName(), signature.getParameterTypes());
//1.3获取方法上的RequiredLog注解内容,获取用户操作
//1.3.1获取目标方法上的注解
RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
//1.3.2获取注解中的内容,执行了什么操作
String operation = requiredLog.value();
//1.4获取目标方法名(类名+方法名)
String targetMethodName = targetClass.getName() + "." + targetMethod.getName();
//1.5获取目标方法执行时传入的参数
String params = new ObjectMapper().writeValueAsString(jp.getArgs());
//1.6获取登录用户名,
String username = (String) SecurityContextHolder.getContext()
.getAuthentication()//认证对象
.getPrincipal();
//1.7获取ip地址,ServletRequestAttributes里边才有getRequest().getRemoteAddr()方法,所以强转
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes ) RequestContextHolder.getRequestAttributes();
String ip = requestAttributes.getRequest().getRemoteAddr();
//2.将用户行为日志,封装到Log对象
Logger log = LoggerFactory.getLogger(LogAspect.class); //@Slf4j 效果一样
Log logInfo = new Log();
logInfo.setIp(ip);
logInfo.setUsername(username);
logInfo.setOperation(operation);
logInfo.setMethod(targetMethodName);
logInfo.setParams(params);
logInfo.setTime(time);
logInfo.setError(error);
logInfo.setStatus(status);
logInfo.setCreatedTime(new Date());
//3.记录用户行为日志(控制台,文件,数据库)
log.debug("==="+logInfo+"===");
remoteLogService.insertLog(logInfo);
}
版权声明:本文标题:微服务版单点登陆系统(SSO)实践 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dongtai/1725736256a1039756.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论