单点登陆系统简介
背景分析
传统的登录系统中,每个站点都实现了自己的专用登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。例如: 这样的系统,我们又称之为多点登陆系统。应用起来相对繁琐(每次访问资源服务都需要重新登陆认证和授权)。与此同时,系统代码的重复也比较高。由此单点登陆系统诞生。
单点登陆系统概述
单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。例如:
单点登陆系统解决方案设计
解决方案1:用户登陆成功以后,将用户登陆状态存储到redis数据库,例如:
说明,在这套方案中,用户登录成功后,会基于UUID生成一个token,然后与用户信息绑定在一起存储到数据库.后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般.
解决方案2:用户登陆成功以后,将用户信息存储到token(令牌),然后写到客户端进行存储。(本次设计方案)
说明,在这套方案中,用户登录成功后,会基于JWT技术生成一个token,用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库.
单点登录系统初步设计
服务设计
基于单点登陆系统中的业务描述,进行初步服务架构设计,如图所示: 其中,服务基于业务进行划分,系统(system)服务只提供基础数据(例如用户信息,日志信息等),认证服务(auth)负责完成用户身份的校验,密码的比对,资源服务(resource)代表一些业务服务(例如我的订单,我的收藏等等).
工程结构设计
基于服务的划分,设计工程结构如下:
SSO父工程创建及初始化
创建父工程
父工程pom文件初始配置
1.添加依赖
<!--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>
系统基础服务工程设计及实现
业务描述
本次设计系统服务(System),主要用于提供基础数据服务,例如日志信息,用户信息等。
表结构设计
系统服务模块,基本表结构设计,例如:
添加SQL文件
在02-sso目录下新建一个sql,然后添加jt-sso.sql
点击去下载
添加完之后需要去数据库执行一下sql文件(小黑窗口 source 路径)
创建系统服务工程
1.创建在sso-system目录下创建sso-system工程 2.添加项目依赖
<dependencies>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis plus (简化mybatis操作)-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--nacos服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos服务配置-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--SpringWeb 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.在项目中添加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: root
logging:
level:
com.jt: debug
4.创建项目启动类
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);
}
}
然后先启动nacos再启动项目,接着访问nacos网站,检查服务是否注册成功
5.添加数据库测试类,测试数据库是否连接成功
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;
@Test
void testGetConnection() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println(connection);
}
}
创建Pojo对象逻辑实现类
package com.jt.system.pojo;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private static final long serialVersionUID = -7842876778505032479L;
private Long id;
private String userName;
private String password;
private String status;
}
序列化id生成方法 点击user然后alt加回车,点击第一个选项
若没有这个选项时: 点击左上角File->Settings->Editor->Inspections 搜索框里查找Seriaizable,找到serialVersionUID打上勾然后应用,然后再试一次
Dao对象逻辑实现
1.创建UserMapper接口,并定义基于用户名查询用户信息
package com.jt.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select id,username,password,status from tb_users where username=#{username}")
User selectUserByUserName(String name);
}
2.创建UserMapperTests类,对业务方法进行测试
package com.jt;
import com.jt.system.dao.UserMapper;
import com.jt.system.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class UserMapperTests {
@Autowired
private UserMapper userMapper;
@Test
void testSelectUserByUsername(){
User user = userMapper.selectUserByUserName("admin");
System.out.println(user);
}
}
Service对象逻辑实现
创建UserService接口及实现类,定义用户及用户权限查询逻辑 1.定义service接口
package com.jt.system.service;
import com.jt.system.pojo.User;
import java.util.List;
public interface UserService {
User selectUserByUserName(String name);
List<String> selectUserPermissions(Long userId);
}
2.定义service接口实现类
package com.jt.system.service.impl;
import com.jt.system.dao.UserMapper;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User selectUserByUserName(String name) {
return userMapper.selectUserByUserName(name);
}
@Override
public List<String> selectUserPermissions(Long userId) {
return userMapper.selectUserPermissions(userId);
}
}
Controller对象逻辑实现
package com.jt.system.controller;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/login/{username}")
public User doSelectUserByUsername(@PathVariable("username") String username){
return userService.selectUserByUserName(username);
}
@GetMapping("/permission/{userId}")
public List<String> doSelectUserPermissions(@PathVariable("userId") Long userId ){
return userService.selectUserPermissions(userId);
}
}
启动服务访问测试
1.使用postman进行测试
2.使用IDEA进行测试
统一认证工程设计及实现
业务描述
用户登录时调用此工程对用户身份进行统一身份认证和授权
创建工程及初始化
1.创建sso-auth工程
2.添加依赖
<dependencies>
<!--SpringWeb-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos服务配置-->
<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>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
3.添加bootstrap.yml配置文件
server:
port: 8071
spring:
application:
name: sso-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
4.创建项目启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class,args);
}
}
启动并访问项目
运行启动类,然后控制台会出现一串密码 然后在浏览器地址栏输入http://localhost:8071,会出现一个登录页面 其中,默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登陆页面,所以会出现404): 若是需要展示页面可以在resources目录下新建一个static目录,并在static目录下新建一个index.html
定义用户信息处理对象
1.定义User对象,用于封装从数据库查询到的用户信息
package com.jt.auth.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1464970784627633041L;
private Long id;
private String username;
private String password;
private String status;
private List<String> permissions;
}
2.定义远程Service对象,用于实现远程用户信息调用
package com.jt.auth.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import com.jt.auth.pojo.User;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(value = "sso-system",contextId = "remoteUserService")
public interface RemoteUserService {
@GetMapping("/user/login/{username}")
User selectUserByUsername(@PathVariable("username") String username);
@GetMapping("/user/permission/{userId}")
List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}
3.定义用户登录业务逻辑处理对象
package com.jt.auth.service;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.jt.auth.pojo.User remoteUser=
remoteUserService.selectUserByUsername(username);
if(remoteUser==null) {
throw new UsernameNotFoundException("user is not exist");
}
final List<String> permissions =
remoteUserService.selectUserPermissions(remoteUser.getId());
log.debug("permissions: {}",permissions.toString());
return new User(username,remoteUser.getPassword(),
AuthorityUtils.createAuthorityList(
permissions.toArray(new String[]{})));
}
}
定义Security配置类
定义Spring Security配置类,在此类中配置认证规则
package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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 SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
基于Postman进行访问测试
启动Nacos,sso-system,sso-auth服务,然后基于postman访问网关,执行登录测试
Security认证流程分析
目前的登陆操作,也就是用户的认证操作,其实现主要基于Spring Security框架其认证简易流程如下:
构建令牌生成及配置对象
本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.
package com.jt.auth.config;
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 {
private static final String SIGNING_KEY = "auth";
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
定义Oauth2认证授权配置
第一步:所有零件准备好了开始拼装最后的主体部分,这个主体部分就是授权服务器的核心配置
package com.jt.auth.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.*;
import java.util.Arrays;
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
private TokenStore tokenStore;
private TokenEnhancer jwtAccessTokenConverter;
private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
.tokenServices(tokenServices());
}
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancerChain);
tokenServices.setAccessTokenValiditySeconds(3600);
tokenServices.setSupportRefreshToken(true);
tokenServices.setRefreshTokenValiditySeconds(3600*5);
return tokenServices;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("gateway-client")
.secret(passwordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password","refresh_token");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
启动postman进行访问测试
登陆访问测试
登录成功以后,会在postman控制台显示如下信息:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzgxNjE0MzUsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI2YTFlM2JlMC00YmRkLTQwYWEtYWUwYi01ZGFlMDc2MGNmMmYiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.fNrcl5wFHxdnm6zTUhCZruEye7IuLa_wxJUSgbSropw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI2YTFlM2JlMC00YmRkLTQwYWEtYWUwYi01ZGFlMDc2MGNmMmYiLCJleHAiOjE2MzgxNzU4MzUsImF1dGhvcml0aWVzIjpbInN5czpyZXM6Y3JlYXRlIiwic3lzOnJlczpsaXN0Iiwic3lzOnJlczpkZWxldGUiXSwianRpIjoiYTg5MGQ1ODQtZGIwMy00NDViLWJhNzktZDJhN2RiYTk5M2QyIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQifQ.KCENeV8OSoXpzXN9aGrK2VXd5rx0RC5-FTBm24HjWkU",
"expires_in": 3599,
"scope": "all",
"jti": "6a1e3be0-4bdd-40aa-ae0b-5dae0760cf2f"
}
检查token信息: 请求访问成功会在postman控制台显示如下信息:
{
"user_name": "admin",
"scope": [
"all"
],
"active": true,
"exp": 1635680023,
"authorities": [
"sys:res:create",
"sys:res:list",
"sys:res:delete"
],
"jti": "ce4aaee8-031f-4ff8-a0fe-e0cd93e8f374",
"client_id": "gateway-client"
}
刷新令牌应用测试: 请求访问成功会在postman控制台显示如下信息:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzgxNjk0MTIsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiIzYzczNWZhZi1jZjYzLTRiYmEtOGUxYy1iOTc2MWZmMGU2ZGEiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.NidtPQDK-H_ULL_PDdP4jeL8drgQDXQgTNIZulmKsdA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIzYzczNWZhZi1jZjYzLTRiYmEtOGUxYy1iOTc2MWZmMGU2ZGEiLCJleHAiOjE2MzgxNzU4MzUsImF1dGhvcml0aWVzIjpbInN5czpyZXM6Y3JlYXRlIiwic3lzOnJlczpsaXN0Iiwic3lzOnJlczpkZWxldGUiXSwianRpIjoiYTg5MGQ1ODQtZGIwMy00NDViLWJhNzktZDJhN2RiYTk5M2QyIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQifQ.6tZNxdNj4W_dBH6_m28grTDmn9GVpWDp2Q4D353h_Ss",
"expires_in": 3599,
"scope": "all",
"jti": "3c735faf-cf63-4bba-8e1c-b9761ff0e6da"
}
资源服务工程设计及实现
服务描述
资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下时受限访问,例如有些资源有用户,都可以访问,有些资源必须认证后才可以访问,有些资源认证后,有权限才可以访问.
业务设计架构
用户访问资源时的认证,授权流程设计如下:
项目创建及初始化
1.创建工程: 2.初始化文件依赖
<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-oauth2</artifactId>
</dependency>
</dependencies>
3.创建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
4.创建启动类
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);
}
}
创建资源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("hasAnyAuthority('sys:res:list')")
@GetMapping("/select")
public String doSelect(){
return "Select Resource OK";
}
@PreAuthorize("hasAnyAuthority('sys:res:delete')")
@DeleteMapping("/delete")
public String doDelete(){
return "Delete Resource OK";
}
@PreAuthorize("hasAnyAuthority('sys:res:create')")
@PostMapping("/insert")
public String doInsert(){
return "Insert Resource OK";
}
@PreAuthorize("hasAnyAuthority('sys:res:update')")
@PutMapping("/update")
public String doUpdate(){
return "Update Resource OK";
}
}
配置令牌解析器对象
package com.jt.resource.config;
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 {
private static final String SIGNING_KEY = "auth";
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
配置资源认证授权规则
package com.jt.resource.config;
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.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@EnableResourceServer
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/resource/doSelect")
.authenticated()
.anyRequest().permitAll();
}
}
启动Postman进行访问测试
不携带令牌访问,例如 携带令牌进行访问,例如 没有访问权限,例如
网关工程设计及实现
业务描述
本次设计中,API网关是服务访问入口,身份认证,资源访问都通过网关进行资源统一转发.
项目创建及初始化
1.创建项目 2.初始化pom文件内容
<dependencies>
<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>
3.创建bootstrap.yml配置文件并进行路由定义
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
predicates:
- Path=/sso/resource
4.定义启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
启动postman进行访问测试
基于网关进行登陆访问测试,例如 基于网关进行资源访问测试
客户端UI工程设计及实现
业务描述
本次项目设计采用前后端分离架构设计,前端工程服务基于springbootWeb服务进行实现
项目创建及初始化
1.创建项目 2.在pom文件中添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.创建启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UIApplication {
public static void main(String[] args) {
SpringApplication.run(UIApplication.class, args);
}
}
创建UI工程登陆页面
1.在resource目录下创建static目录 2.在static目录下新建js目录 3.在就是目录下添加axios.js
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
4.在static目录下创建登陆页面login.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>login</title>
</head>
<body>
<div class="container"id="app">
<h3>Please Login</h3>
<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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="js/axios.js"></script>
<script>
var vm=new Vue({
el:"#app",
data:{
username:"",
password:""
},
methods: {
doLogin() {
let url = "http://localhost:9000/sso/oauth/token"
let params = new URLSearchParams()
params.append('username',this.username);
params.append('password',this.password);
params.append('client_id',"gateway-client");
params.append('client_secret',"123456");
params.append('grant_type',"password");
axios.post(url, params)
.then((response) => {
alert("login ok")
let result=response.data;
console.log("result",result);
localStorage.setItem("accessToken",result.access_token);
location.href="/resource.html";
})
.catch((e)=>{
console.log(e);
})
}
}
});
</script>
</body>
</html>
5.打开浏览器进行访问测试
创建资源展现页面
1.在UI工程的static目录下创建resource.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>The Resource Page</h1>
<button onclick="doSelect()">查询我的资源</button>
<button onclick="doUpdate()">修改我的资源</button>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="js/axios.js"></script>
<script>
function doSelect(){
let url="http://localhost:9000/sso/resource/select";
let token=localStorage.getItem("accessToken");
axios.get(url,{headers:{"Authorization":"Bearer "+token}})
.then(function (response){
alert("select ok")
console.log(response.data);
})
.catch(function (e){
if(e.response.status==401){
alert("请先登录");
location.href="/login.html";
}else if(e.response.status==403){
alert("您没有权限")
}
console.log("error",e);
})
}
function doUpdate(){
let url="http://localhost:9000/sso/resource/update";
let token=localStorage.getItem("accessToken");
console.log("token",token);
axios.put(url,"",{headers:{"Authorization":"Bearer "+token}})
.then(function (response){
alert("update ok")
console.log(response.data);
})
.catch(function (e){
console.log(e);
if(e.response.status==401){
alert("请先登录");
location.href="/login.html";
}else if(e.response.status==403){
alert("您没有权限")
}
console.log("error",e);
})
}
</script>
</body>
</html>
2.打开浏览器进行访问测试(登陆前和登陆后检查点击如下按钮检测结果)
SSO微服务工程中用户行为日志的记录
系统需求分析
业务描述
用户在sso-resource工程访问我们的资源数据时,获取用户的行为日志信息,然后传递给sso-system工程,将日志信息存储到数据库. 业务架构分析
系统服务中的日志存储服务
业务描述
本次设计中,系统服务负责将其他服务获取的用户行为日志写入到数据库.
Pojo逻辑实现
定义一个Log对象,用于在内存中存储用户行为日志信息
package com.jt.system.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("tb_logs")
public class Log implements Serializable {
private static final long serialVersionUID = 1300330213216486658L;
@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;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@TableField("createdTime")
private Date createdTime ;
private Integer status;
private String error;
}
Dao逻辑实现
1.创建用户行为日志数据层对象,用于处理数据持久层逻辑
package com.jt.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.Log;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogMapper extends BaseMapper<Log> {
}
2.定义单元测试,对数据层方法进行单元测试
package com.jt;
import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
public class LogMapperTests {
@Autowired
private LogMapper logMapper;
@Test
void testInsert(){
Log log = new Log();
log.setUsername("cgb2107");
log.setIp("192.168.122.123");
log.setOperation("查询资源");
log.setMethod("pkg.ResourceController.doSelect");
log.setParams("");
log.setStatus(1);
log.setTime(100L);
log.setCreatedTime(new Date());
logMapper.insert(log);
}
}
Service逻辑实现
1.定义日志业务接口
package com.jt.system.service;
import com.jt.system.pojo.Log;
public interface LogService {
void insertLog(Log log);
}
2.定义日志业务接口实现类
package com.jt.system.service.impl;
import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Async
@Override
public void insertLog(Log log) {
String tName = Thread.currentThread().getName();
System.out.println("LogServiceImpl.thread.name->"+tName);
logMapper.insert(log);
}
}
Controller逻辑实现
package com.jt.system.controller;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/log")
public class LogController {
@Autowired
private LogService logService;
@PostMapping
public void doInsertLog(@RequestBody Log log){
System.out.println("LogController.doInsertLog->"+Thread.currentThread().getName());
logService.insertLog(log);
}
}
2.注意: 在sso-system服务的启动类中添加这个注解
3.启动服务,基于postman进行访问测试
资源服务中行为日志操作设计
业务描述
在不修改目标业务方法代码实现的基础上,访问目标方法时,获取用户行为日志.
Pojo逻辑对象定义
定义日志对象,用户封装获取到的用户行为日志
package com.jt.resource.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class Log implements Serializable {
private static final long serialVersionUID = 1300330213216486658L;
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;
}
切入点注解定义
构建一个自定义注解,名字为RequiredLog,后续会基于此注解描述作为切入点,定义切入点方法
package com.jt.resource.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String value() default "";
}
AOP方式获取并记录日志
定义一个日志切面,基于此切面中的通知方法实现用户行为日志的获取和记录
package com.jt.resource.aspect;
import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import com.jt.resource.service.RemoteResourceService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Date;
@Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
public void doLog(){
}
@Around("doLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
int status = 1;
String error = "";
long time = 0L;
long t1 = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long t2 = System.currentTimeMillis();
time = t2 - t1;
return result;
}catch (Throwable e){
long t3 = System.currentTimeMillis();
time = t3 - t1;
status = 0;
error = e.getMessage();
throw e;
}finally {
saveLog(joinPoint,time,status,error);
}
}
private void saveLog(ProceedingJoinPoint joinPoint,long time,int status,String error) throws NoSuchMethodException, IOException {
Class<?> targetClass = joinPoint.getTarget().getClass();
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method targetMethod = targetClass.getDeclaredMethod(signature.getName(),signature.getParameterTypes());
RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
String operation = requiredLog.value();
String targetMethodName = targetClass.getName() + "." + targetMethod.getName();
String params = new ObjectMapper().writeValueAsString(joinPoint.getArgs());
String username = (String)SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip = requestAttributes.getRequest().getRemoteAddr();
Log logInfo = new Log();
logInfo.setIp(ip);
logInfo.setUsername(username);
logInfo.setOperation(operation);
logInfo.setMethod(targetMethodName);
logInfo.setParams(params);
logInfo.setTime(time);
logInfo.setStatus(status);
logInfo.setError(error);
logInfo.setCreatedTime(new Date());
System.out.println("LogInfo: " + logInfo);
}
}
启动服务进行访问测试
依次启动nacos,sso-system,sso-auth,sso-resource,sso-gateway,sso-ui工程服务,然后执行登陆,登陆成功后查询我的资源,检测日志输出。
服务中AOP技术应用原理分析
AOP是一种设计思想,它要实现的功能就是"锦上添花",就是在尽量不修改原有目标方法的基础上,添加一些扩展功能,例如日志的记录,权限的控制,事务的控制,异步任务的执行等等,其应用原理如图所示: 说明:当我们在项目中定义了AOP切面以后,系统启动时,会对有@Aspect注解描述的类进行加载分析,基于切入点的描述为目标类型对象,创建代理对象,并在代理对象内部创建一个执行链,这个执行链中包含拦截器(封装了切入点信息),通知(Around,…),目标对象等,我们请求目标对象资源时,会直接按执行链的顺序对资源进行调用。
Fegin方式将日志传递给系统服务
1.确保sso-resource工程中添加了openfign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.确保sso-resource工程中的启动类上添加了@EnableFeginClients注解
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class ResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class,args);
}
}
3.定义日志远程服务调用接口
package com.jt.resource.service;
import com.jt.resource.pojo.Log;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "sso-system",contextId = "remoteResourceService")
public interface RemoteResourceService {
@PostMapping("/log")
void insertLog(@RequestBody Log log);
}
4.在LogAspect中注入RemoteLogService对象,并通过此对象将日志对象传递到sso-system服务 5.依次启动服务进行访问测试
小知识扩展
在IDEA里执行SQL语句
在IDEA里测试,发送带有请求头的URL
|