Spring Security Oauth2.0认证授权
-
Spring Security:安全框架
-
OAuth2.0:用于分布式认证授权
-
JWT:与OAuth2.0相关的令牌
1、基本概念
1.1什么是认证
- 输入账号和密码登录微信的过程就是认证
- 判断用户身份合法的过程
系统为什么要认证
- 保护系统隐私数据和资源
1.2什么是会话
- 为了避免用户的每次操作都进行认证可将用户的信息保存在会话中
- 保持当前用户登录状态所提供 的机制
输入支付密码,是二次认证
会话的2种方式
1:基于session方式
- 保存到服务端cookie中
- 每次请求带session_id
2:基于token方式
- 客户端可以放cookie中,或者放在localStorage中存储,服务端大可不必存token(基于JWT校验)
- 每次请求带token
1.3什么是授权
- 保证资源是否有权限使用
比如微信认证(登录)后,是否绑定银行卡==>是否可以发红包
授权的2种资源
-
功能资源:系统菜单、页面、按钮、代码方法
-
实体(数据)资源:2类
- 资源类型:商品信息
- 资源实例:商品编号为001的商品
who(主体) what(资源) how(权限)
1.4授权的数据模型
1.4.1 授权的数据模型6张表
- 主体:(用户id、账号、密码、…)
- 资源:(资源id、资源名称、访问地址、…)
- 权限:(权限id、权限标识、权限名称、资源id、…)
- 角色:(角色id、角色名称、…)
- 角色和权限的关系:(角色id、权限id)
- 主体和角色的关系:(用户id、角色id、…)
- 主体、资源、权限关系图如下
以上是6张数据表的关系
1.4.2 授权的数据模型5张表(优化)
优化后的授权数据模型
- 权限和资源是多对一
- 合并权限和资源表
- 权限:(权限id、权限标识、权限名称、资源名称、资源访问地址、…)
- 修改后数据模型之间的关系如下图:
1.5 RBAC实现授权
企业常用RBAC实现授权
1.5.1 基于角色的访问控制
按角色进行授权(Role-Based Access Controller)
案例:
if(主体.hasRole("总经理角色id")){
查询工资
}
问题:如果查询工资的角色变化为总经理和部门经理,还有项目经理可访问,此时就需要修改判断逻辑
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id") || 主体.hasRole("项目经理角色id")){
查询工资
}
根据上边案例发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可拓展性差
1.5.2 基于资源的访问控制
按资源进行授权(Resource-Based Access Controller)
代码案例:
if(主体.hasPermission("查询公司权限标识")){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化,都不需要修改授权代码,系统可扩展性强
2、基于session的认证方式
2.1 认证流程
2.2 springmvc session认证
-
自己写登录页面
-
自己写登录时,校验账户和密码
-
自己写拦截器
-
拦截器进行拦截请求,资源分配
2.3 Spring security 快速上手
- 不需要自己设置拦截器
- 自带登录界面
- 帮助校验账户和密码:UserDetailService
- 需要初始化
有包的版本问题,建议和项目上一致
<packaging>war</packaging>//spring是基于war包形式的
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
<build>
<finalName>security-springmvc</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<filtering>true</filtering>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
2.4 Spring security集成Spring boot
-
导入依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!--以下是springboot依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--以下是jsp依赖--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <!--jsp页面的jstl标签--> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <!--用于编译jsp--> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> </dependencies> <build> <finalName>security-springboot</finalName> <pluginManagement> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>utf-8</encoding> <useDefaultDelimiters>true</useDefaultDelimiters> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>**/*</include> </includes> </resource> <resource> <directory>src/main/java</directory> <filtering>true</filtering> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </configuration> </plugin> </plugins> </pluginManagement> </build>
server:
port: 8080
servlet:
context-path: /security-springboot #工程路径
spring:
application:
name: security-springboot #工程名字
#视图解析器
mvc:
view:
prefix: /WEB-INF/views/ #前缀
suffix: .jsp #后缀
springboot项目不需要 applicationContext.xml (spring的配置)----用application.yaml代替
- spring容器需要初始化
- springboot不需要
3、spring security的工作原理
spring security解决的问题就是安全访问控制
spring security对web资源的保护是靠多个Filter实现的
3.1 认证的流程
5个数据表
/*
Navicat Premium Data Transfer
Source Server : lsx_localhost
Source Server Type : MySQL
Source Server Version : 50528
Source Host : localhost:3306
Source Schema : user_db
Target Server Type : MySQL
Target Server Version : 50528
File Encoding : 65001
Date: 28/09/2020 22:18:51
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_permission
-- ----------------------------
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `code`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_permission
-- ----------------------------
INSERT INTO `t_permission` VALUES (1, 'p1', '测试资源1', '/r/r1');
INSERT INTO `t_permission` VALUES (2, 'p2', '测试资源2', '/r/r2');
INSERT INTO `t_permission` VALUES (3, 'p3', '测试资源3', '/r/r3');
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` bigint(32) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` datetime NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `role_name`(`role_name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES (1, 'root', '管理员', NULL, NULL, NULL);
INSERT INTO `t_role` VALUES (2, 'user', '员工', NULL, NULL, NULL);
-- ----------------------------
-- Table structure for t_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`role_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_role_permission
-- ----------------------------
INSERT INTO `t_role_permission` VALUES (1, 1);
INSERT INTO `t_role_permission` VALUES (1, 2);
INSERT INTO `t_role_permission` VALUES (1, 3);
INSERT INTO `t_role_permission` VALUES (2, 1);
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户姓名',
`mobile` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, 'lsx', '$2a$10$0eNRxImm.3JuvSOxR9vz6uqZ3n9V26pSG2yHw7JFRjhSI9GFhjhNy', '张三', '15675288363');
INSERT INTO `t_user` VALUES (2, 'carter', '$2a$10$0eNRxImm.3JuvSOxR9vz6uqZ3n9V26pSG2yHw7JFRjhSI9GFhjhNy', '李四', '15675288363');
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
`create_time` datetime NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES (1, 1, NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;
4、授权
- web授权----进行url拦截
- 方法授权—到Controller中进行注解添加
注解
@PreAuthorize :方法执行前拦截
@PostAuthorize:方法执行后拦截
@Secured:不推荐使用
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)//开启3种注解
5、分布式系统认证方案
共享性(统一)、开放性(第三方)
5.1 分布式认证需求
分布式每个服务都有认证、授权的需求,每个服务都实现一套认证逻辑会非常冗余
-
统一的认证授权
- 要实现统一认证方式可扩展,不同类型用户,进行统一认证授权,支持各种认证需求,比如:用户密码认证、短信验证码、二维码、人脸识别等认证方式
-
应用接入认证
- 支持第三方应用接入,供应扩展和开放能力
5.2选型分析
-
基于session的认证方式
Session复制:多台应用服务企器之间同步session,使session保持一致性,对外透明
Session黏贴:当用户访问集群中某台服务器后,强制指定后续的所有请求均落到此机器上
Session集中存储:将session存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取session
优点:更好的在服务端对会话进行控制,且安全性高
缺点:session机制方式基于cookie,无法跨域,另外随着系统的扩展,需要提高session复制、黏贴、和集中式存储的容错性
-
基于token的认证方式
优点:容易维护、扩展性强
缺点:token包含的数据量大,每次请求都需要传递,占宽带。token签名验证对cpu产生处理负担
5.3 oauth2.0是什么
-
专门为第三方系统认证所设计的认证协议
-
允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息
-
JWT令牌格式
案例:微信登录(第三方授权访问)
6、spring cloud security oauth2.0介绍
- 授权服务 UAA
- 资源服务 Order、Product、…
6.1 创建maven父工程
<!--父工程对依赖进行管理 所以是个pom文件-->
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>1.8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<filtering>true</filtering>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
6.2 创建maven子工程
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!--spring boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!--spring cloud security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--oauth2.0第三方授权-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--jwt令牌格式-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--数据jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
6.3 创建资源工程
<dependencies>
<!--spring boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--oauth2.0第三方授权-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
6.4授权服务器配置
6.5 配置授权端点的url
6.5.1 默认的url
7、授权码模式
- 授权服务配置AuthorizationServer
/**
* 授权服务配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
//1:客户端信息服务
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//内存中
.withClient("c1")//客户端唯一标识
.secret(new BCryptPasswordEncoder().encode("secret"))//客户端秘钥
.resourceIds("res1")//资源标识
//authorization_code:授权码 password:密码模式 client_credentials客户端模式 implicit:简要模式 refresh_token:刷新令牌
.authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")//授权类型
.scopes("all")//允许范围
.autoApprove(false)//跳转到授权页面
.redirectUris("http://www.baidu.com");//客户端会跳地址
}
//2:管理令牌服务
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setTokenStore(tokenStore);
services.setSupportRefreshToken(true);
services.setAccessTokenValiditySeconds(7200);//令牌默认有效期2小时
services.setRefreshTokenValiditySeconds(259200);//刷新令牌默认有效期3天
return services;
}
//设置 【授权码模式】 的授权服务
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
//3:令牌的访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理器---WebSecurityConfig中配置
.authorizationCodeServices(authorizationCodeServices)//授权码服务
.tokenServices(tokenServices())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//4:安全约束---拦截器
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")//这个url是公开的---用户不用登陆直接使用 url:/oauth/token_key
.checkTokenAccess("permitAll()")//远程校验token 的合法性 url:/oauth/check_token
.allowFormAuthenticationForClients();//允许表单申请令牌
}
}
- 令牌管理配置TokenConfig
//令牌管理
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}
- WebSecurityConfig认证配置
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)//开启方法授权注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//认证管理器
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManagerBean();
}
//定义密码编码器---spring security自带加密
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
//拦截用户的请求---安全拦截截止(拦截器)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//屏蔽csrf请求问题
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
- Service层
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
private UserDao userDao;
//根据账号 查询用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("前端用户输入的账号---->"+username);
//连接数据库 根据账号查询用户信息
UserDto userDto = userDao.getUserByUsername(username);
if (userDto == null){
return null;
}
//根据用户的id查询用户的权限
List<String> permissions = userDao.findPermissionByUserId(userDto.getId());
//将它转成数组
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
//将权限标识符添加到UserDetails的authorities中区
UserDetails userDetails = User.withUsername(userDto.getUsername())
.password(userDto.getPassword())
.authorities(permissionArray).build();
return userDetails;
}
}
- dao层
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//根据账号 查询用户信息
public UserDto getUserByUsername(String username){
String sql = "select id,username,password,fullname,mobile from t_user where username = ?";
List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
if (list != null && list.size() == 1){
return list.get(0);
}
return null;
}
//根据用户的id查询用户的权限
public List<String> findPermissionByUserId(String userId){
String sql ="select * from t_permission WHERE id IN(\n" +
"select permission_id from t_role_permission where role_id IN(\n" +
"\tselect role_id from t_user_role where user_id = ? \n" +
") \n" +
")";
List<PermissionDto> query = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
List<String> list = new ArrayList<>();
query.forEach(c -> list.add(c.getCode()));//传入权限标识符号
return list;
}
}
- pojo层
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PermissionDto {
private String id;
private String code;
private String description;
private String url;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
7.启动类
@SpringBootApplication
@EnableDiscoveryClient//服务注册与发现
@EnableHystrix//熔断
@EnableFeignClients(basePackages = {"com.carter.security.distributed.uaa"})//远程调用
public class UaaSpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(UaaSpringbootApplication.class,args);
}
}
- 浏览器访问地址(参数自己在ClientDetailsServiceConfigurer中定义的)拿到授权码
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
- 客户端拿到授权码向 授权服务器 索要访问access_key申请令牌
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5Pgfsc&redirect_uri=http://www.baidu.com
7.1创建表
Drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(255) PRIMARY KEY,
resource_ids VARCHAR(255),
client_secret VARCHAR(255),
scope VARCHAR(255),
authorized_grant_types VARCHAR(255),
web_server_redirect_uri VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information TEXT,
create_time timestamp default now(),
archived tinyint(1) default '0',
trusted tinyint(1) default '0',
autoapprove VARCHAR (255) default 'false'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Drop table if exists oauth_code;
create table oauth_code (
create_time timestamp default now(),
code VARCHAR(255),
authentication BLOB,
index code_index (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第三方认证通过
安全性最高
8、简化模式授权
Get请求直接获取
/uaa/oauth/authorize?
client_id=c1&
response_type=token&
scope=all&
redirect_uri=http://www.baidu.com
9、JWT介绍
基于token的认证方式,每次都需要进行远程调用密码校验,影响性能
所以采用JWT令牌方式解决此问题(JSON WEB TOKEN),本身存储签名信息
JWT令牌
优点:
1)jwt基于json,非常方便解析
2)可以在令牌中自定义丰富的内容,易拓展
3)通过非对称加密算法以及数字签名技术,JWT防止篡改,安全性高
4)资源服务使用JWT不依赖认证服务即可完成授权
缺点:
JWT令牌比较长,占用存储空间比较大
头部信息+内容+签名(防止篡改)
9.1 测试JWT密码模式获取令牌
Post密码模式获取令牌 【仅用于内测人员】
9.2 测试授权码模式获取令牌
Get获取授权码code
http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
POST申请令牌
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5Pgfsc&redirect_uri=http://www.baidu.com
9.3 测试简化模式获取令牌
GET请求
/uaa/oauth/authorize?
client_id=c1&
response_type=token&
scope=all&
redirect_uri=http://www.baidu.com
10、spring security实现分布式系统授权
7.1 注册中心创建
distributed-security-discovery
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
配置文件
spring:
application:
name: distributed-discovery
server:
port: 53000 #启动端口
eureka:
server:
enable-self-preservation: false #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
eviction-interval-timer-in-ms: 10000 #清理间隔(单位毫秒,默认是60*1000)5秒将客户端剔除的服务在服务注册列表中剔除#
shouldUseReadOnlyResponseCache: true #eureka是CAP理论种基于AP策略,为了保证强一致性关闭此切换CP 默认不关闭 false关闭
client:
register-with-eureka: false #false:不作为一个客户端注册到注册中心
fetch-registry: false #为true时,可以启动,但报异常:Cannot execute request on any known server
instance-info-replication-interval-seconds: 10
serviceUrl:
defaultZone: http://localhost:${server.port}/eureka/
instance:
hostname: ${spring.cloud.client.ip-address}
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
启动类
@SpringBootApplication
@EnableEurekaServer //这是一个注册中心Eureka的服务
public class DiscoveryApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryApplication.class,args);
}
}
distributed-security-order资源的配置
<!--Eureka客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring.application.name=order-service
server.port=53021
spring.main.allow-bean-definition-overriding = true
#日志
logging.level.root = debug
logging.level.org.springframework.web = info
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x-forwarded-for
server.tomcat.protocol_header = x-forwarded-proto
server.use-forward-headers = true
server.servlet.context-path = /order
#视图信息
spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request-context-attribute = rc
spring.freemarker.content-type = text/html
spring.freemarker.charset = UTF-8
spring.mvc.throw-exception-if-no-handler-found = true
spring.resources.add-mappings = false
#注册中心
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
#远程调用
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
uaa服务同上。。。
7.2 网关创建
-
distributed-security-gateway
<dependencies>
<!--注册中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--熔断-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--负载-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--远程调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--切面-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<!--重试框架-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!--健康监控)配置和使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--web项目-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--安全-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--oauth2第三方应用接入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--jwt令牌-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<!--拦截器-->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<!--序列化-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--快速注解-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true
logging.level.root = info
logging.level.org.springframework = info
zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *
zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**
zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
启动类
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class,args);
}
}
7.2.1网关整合OAuth2.0有两种思路
1:一种是认证服务器UAA生成jwt令牌,所有请求在网关层验证,判断权限等操作
2:一种是由各资源服务处理,网关只做请求转发
-
TokenConfig配置jwt令牌模式
//令牌管理 @Configuration public class TokenConfig { private String SIGNING_KEY = "uaa123"; @Bean public TokenStore tokenStore() { //JWT令牌存储方案---资源服务器不需要进行远程校验 return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(SIGNING_KEY); //对称秘钥,【资源服务器】使用该秘钥来验证 return converter; } }
-
ResourceServerConfig配置资源验证
@Configuration
public class ResourceServerConfig {
public static final String RESOURCE_ID = "res1";
//uaa资源服务配置
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uaa/**").permitAll();
}
}
//order资源服务配置
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
//服务进来直接进行验证
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
}
}
//配置其它的资源服务..
}
- WebSecurityConfig配置放行所有url请求
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//放行所有
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
- AuthFilter配置明文令牌toekn转发给微服务的过滤器
public class AuthFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
//请求之前的拦截
@Override
public String filterType() {
return "pre";
}
//优先级
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() throws ZuulException {
//从安全的上下文提取对象
RequestContext context = RequestContext.getCurrentContext();
//取出对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2Authentication)){
return null;//表示不是OAuth2格式的令牌
}
//转成OAuth2格式
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
//获取当前用户对象
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
//1、获取当前用户的身份信息
String principal = userAuthentication.getName();
//2、获取当前用户的权限信息
List<String> authorities = new ArrayList<>();
userAuthentication.getAuthorities().stream().forEach(c->authorities.add(c.getAuthority()));//通过stream流的方式进行权限遍历
//2.1 其他oauth2.0请求的数据
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();//得到所有的参数信息
Map<String,Object> jsonToken = new HashMap<>(requestParameters);//后期可能会放对象所以用Object
if (userAuthentication != null){
jsonToken.put("principal",principal);//身份
jsonToken.put("authorities",authorities);//权限
}
//3、把身份信息和权限信息放到json中,加入http的header中,转发给微服务
context.addZuulRequestHeader("json-Token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
- 转Base64的方法类
public class EncryptUtil {
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
public static String encodeBase64(byte[] bytes){
String encoded = Base64.getEncoder().encodeToString(bytes);
return encoded;
}
public static byte[] decodeBase64(String str){
byte[] bytes = null;
bytes = Base64.getDecoder().decode(str);
return bytes;
}
public static String encodeUTF8StringBase64(String str){
String encoded = null;
try {
encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
logger.warn("不支持的编码格式",e);
}
return encoded;
}
public static String decodeUTF8StringBase64(String str){
String decoded = null;
byte[] bytes = Base64.getDecoder().decode(str);
try {
decoded = new String(bytes,"utf-8");
}catch(UnsupportedEncodingException e){
logger.warn("不支持的编码格式",e);
}
return decoded;
}
public static String encodeURL(String url) {
String encoded = null;
try {
encoded = URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLEncode失败", e);
}
return encoded;
}
public static String decodeURL(String url) {
String decoded = null;
try {
decoded = URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLDecode失败", e);
}
return decoded;
}
public static void main(String [] args){
String str = "abcd{'a':'b'}";
String encoded = EncryptUtil.encodeUTF8StringBase64(str);
String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
System.out.println(str);
System.out.println(encoded);
System.out.println(decoded);
String url = "== wo";
String urlEncoded = EncryptUtil.encodeURL(url);
String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
System.out.println(url);
System.out.println(urlEncoded);
System.out.println(urlDecoded);
}
}
7.3 资源如何处理网关传过来的明文token令牌
- 客户端访范围
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.tokenStore(tokenStore)//jwt令牌
// .tokenServices(tokenService())//验证令牌的服务---远程调用解析
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
//资源服务令牌解析服务---远程调用解析
/* @Bean
public ResourceServerTokenServices tokenService() {
//使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
RemoteTokenServices service=new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
service.setClientId("c1");
service.setClientSecret("secret");
return service;
}*/
}
- jwt令牌策略
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
/* @Bean
public TokenStore tokenStore() {
//使用内存存储令牌(普通令牌)
return new InMemoryTokenStore();
}*/
}
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/r/r1").hasAuthority("p2") web授权访问形势
// .antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll();//除了/r/**,其它的请求可以访问
}
}
-
资源的过滤器TokenAuthenticationFilter
解析头文件中的token,将一些信息给security
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//解析出头中的token
String token = httpServletRequest.getHeader("json-token");
if(token!=null){
String json = EncryptUtil.decodeUTF8StringBase64(token);
//将token转成json对象
JSONObject jsonObject = JSON.parseObject(json);
//用户身份信息
UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
//用户权限
JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
//将用户信息和权限填充 到用户身份token对象中
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//将authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
解码工具类
public class EncryptUtil {
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
public static String encodeBase64(byte[] bytes){
String encoded = Base64.getEncoder().encodeToString(bytes);
return encoded;
}
public static byte[] decodeBase64(String str){
byte[] bytes = null;
bytes = Base64.getDecoder().decode(str);
return bytes;
}
public static String encodeUTF8StringBase64(String str){
String encoded = null;
try {
encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
logger.warn("不支持的编码格式",e);
}
return encoded;
}
public static String decodeUTF8StringBase64(String str){
String decoded = null;
byte[] bytes = Base64.getDecoder().decode(str);
try {
decoded = new String(bytes,"utf-8");
}catch(UnsupportedEncodingException e){
logger.warn("不支持的编码格式",e);
}
return decoded;
}
public static String encodeURL(String url) {
String encoded = null;
try {
encoded = URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLEncode失败", e);
}
return encoded;
}
public static String decodeURL(String url) {
String decoded = null;
try {
decoded = URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLDecode失败", e);
}
return decoded;
}
public static void main(String [] args){
String str = "abcd{'a':'b'}";
String encoded = EncryptUtil.encodeUTF8StringBase64(str);
String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
System.out.println(str);
System.out.println(encoded);
System.out.println(decoded);
String url = "== wo";
String urlEncoded = EncryptUtil.encodeURL(url);
String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
System.out.println(url);
System.out.println(urlEncoded);
System.out.println(urlDecoded);
}
}
头信息对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
/**
* 用户id
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 手机号
*/
private String mobile;
/**
* 姓名
*/
private String fullname;
}
- 资源处获取
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
public String r1(){
//获取用户身份信息
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDTO.getFullname()+"访问资源1";
}
}
11、进行集成测试
- 启动所有的类
- 申请令牌
- 校验令牌
- 进行资源访问
12、总结
12.1 spring security中网关与微服务的关系
- 网关只进行token转发
- 微服务进行授权校验
12.2 Userdetails的扩展方式
1:修改UserDetails对象
2:转成json对象,放到令牌中
12.3 springMvc如何进行资源的认证和授权?
- 在springmvc中,我们通过继承HandlerInterceptor拦截器,在url访问资源前进行权限匹配
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
//访问资源前拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在这个方法中校验用户请求的url是否在用户的权限范围内
//取出用户的身份的信息
Object attribute = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
if (attribute == null){
//没有认证,提示登录
writeContent(response,"请登录");
}
UserDto userDto = (UserDto) attribute;
//请求的url
String requestURI = request.getRequestURI();
//用户权限匹配
if (userDto.getAuthorities().contains("p1") && requestURI.contains("r/r1")){
return true;
}
if (userDto.getAuthorities().contains("p2") && requestURI.contains("r/r2")){
return true;
}
writeContent(response,"没有权限,拒绝访问");
return false;
}
//响应信息给客户端的方法
private void writeContent(HttpServletResponse response, String msg) throws IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(msg);
writer.close();
response.flushBuffer();//清缓存
}
}
- 匹配成功进行认证,认证通过将session_key存到session域中
@RequestMapping(value = "/login",produces = "text/plaint;charset=utf-8")//文本格式
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
//authenticationRequest客户端输入的账户+密码
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session(服务端内存中)
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getFullname()+"登录成功";
}
- 当用户注销登录时,清空session域就行了
@GetMapping(value = "/logout",produces = "text/plaint;charset=utf-8")
public String logout(HttpSession session){
session.invalidate();//清空session
return "退出成功";
}
12.4 spring security框架如何进行资源认证和授权
- WebSecurityConfigurerAdapter【Web安全配置适配器】
12.5 spring boot security如何进行认证和授权
- 客户身份信息认证
- 安全拦截
- 开启方法形式的认证
@RequestMapping(value = "/r/r1",produces = "text/plaint;charset=utf-8")
@PreAuthorize("hasAnyAuthority('p1')")//拥有p1权限才可访问
public String r1(){
return "资源r1访问 +1";
}
12.6 spring cloud 微服务+分布式 security+oauth2.0如何进行认证和授权
-
通过单独的UAA认证服务进行认证和安全拦截
所有的登录认证请求必须经过此服务器—>令牌获取AuthorizationServer
远程资源拦截机制
-
网关将uaa认证服务的令牌进行发送
WebSecurityConfigurerAdapter网络安全适配器:放行所有请求
ResourceServerConfigurerAdapter资源服务配置器:有哪些微服务资源—>用户需要什么样的权限才能使网关发配token
ZuulConfig网关配置:允许跨域+优先级配置
ZuulFilter网关过滤器:通过过滤器对token令牌进行明文分配—>最后才能访问微服务
#/uaa/**开头的url通过uaa-service进行调用
zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**
zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**
-
各个微服务资源如何获取令牌进行认证和授权
OncePerRequestFilter【每个请求过滤一次】
ResourceServerConfigurerAdapter资源服务适配器:进行令牌认证+授权
- 进行security授权
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
public String r1(){
//获取用户身份信息
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDTO.getFullname()+"访问资源1";
}
}
spring security oauth2.0认证授权实例
1、用户认证技术方案
1.1 单点登录技术方案
-
用于分布式系统—>只登陆一次就可以访问所有有权限的服务,不需要每个系统都做一套认证系统
-
制作一套认证系统—>将认证从各个功能中抽离出来
-
利用Redis存储用户的身份
-
访问所有系统—>认证系统—>从Redis中查找token
1.2 单点登录的特点
- 认证系统是独立的系统
- 各个系统通过http或者其他协议与认证系统进行通信
- 用户身份信息存储在Redis集群中
Java能实现单点登录的认证框架:
- Apache Shiro
- CAS
- Spring security CAS
2、第三方认证技术方案
2.1 Oauth2.0认证技术方案
- 主要研究授权码+密码的认证模式
2.2 spring security oauth2.0工作流程图
2.3 spring security oauth2.0授权码服务,资源服务授权流程
- UAA授权服务利用【私钥】生成令牌?
- 资源服务利用【公钥】进行校验令牌?
- 导入oauth2认证依赖
- 导入ResourceConfig配置
- 导入public.key公钥
3、spring security oauth2.0密码模式授权
3.1密码模式特点
- 不需要授权码
- 参数
-
grant_type:密码模式授权填password
-
username:账号
-
password:密码
POST请求:http://localhost:8080/auth/oauth/token
- 此链接需要使用http Basic认证+客户端id+客户端password
AUU认证服务接收到申请令牌的请求---->调用UserDetailsService接口—>将信息放入token中
3.2 UserDetailsService接口
- 客户端验证
- 用户信息验证
3.3 密码模式令牌生成好后的操作
GET: http://localhost:40400/auth/oauth/check_token?token 【校验令牌】
登录时:密码模式授权
访问资源时:校验令牌
令牌过期时:刷新令牌
4、spring security JWT
- 不需要资源Web远程校验【web请求】 客户端—>资源服务—>UAA认证服务 :性能低下
- 直接资源服务自己校验 客户端—>资源服务
4.1 JWT令牌如何校验
- JWT可以使用HMAC算法或使用RAS的公钥和私钥进行数字签名校验,防止被篡改
4.2 JWT令牌结构
-
Header
包括令牌的类型+哈希算法
{ "alg": "HS256", "typ": "JWT" }
-
Payload负载
存放有效信息类容
{ "sub": "1234567890", "name": "456", "admin": true }
-
Signature签名
防止jwt内容被篡改该
使用Base64Url编码
header+"."+payload—>数字签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)//secret唯一标识
4.3 创建JWT令牌的准备工作
密钥证书:公钥+私钥
JDK生成密钥证书:
keytool -genkeypair -alias carterkey -keyalg RSA -keypass root -keystore carter.keystore -storepass rootstore
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码
- 查看证书信息
keytool -list -keystore carter.keystore
4.4 利用私钥生成令牌
利用证书—>私钥+内容—>jwt
4.5 利用公钥校验令牌
公钥错误—>生成JWT令牌错误(spring security已经做)
5、Redis存储JWT令牌
- 是否过期
//读取过期时间,已过期返回‐2
Long expire = stringRedisTemplate.getExpire(key);
6、spring security oauth2.0登录注册案例
6.1 什么时候发放令牌
6.2 客户端id—参数放在哪里?
在application.yml中配置参数
auth:
tokenValiditySeconds: 1200 #token存储到redis的过期时间
clientId: XcWebApp #客户端id
clientSecret: XcWebApp #客户端密码
cookieDomain: localhost #域名
cookieMaxAge: ‐1 #浏览器关闭cookie无效
6.3 原远程生成令牌代码实现
【AUU认证服务】里面集成了spring security
- 远程请求 spring security 生成令牌
- 远程调用userLogin方法
userLogin方法用来申请令牌(账号+密码+客户端id)
- AUU服务的Controller层
/userLogin
参数:
账号username和密码password
client ID是yaml文件中的
作用: 将令牌存储到cookie
cookie:【response,域名,根路径,name:uid,token,cookie有效期,httpOnly:false浏览器可访问cookie】
域名:carter.com—指向—>localhost:8080 相当网关的反向代理 nginx技术
- Service层
客户端id—>yaml中存取
将令牌存到cookie中
//认证方法
public AuthToken login(String username,String password,String clientId,String clientSecret){
//申请令牌
AuthToken authToken = applyToken(username,password,clientId, clientSecret);
if(authToken == null){
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
}
//将 token存储到redis
String access_token = authToken.getAccess_token();
String content = JSON.toJSONString(authToken);
boolean saveTokenResult = saveToken(access_token, content, tokenValiditySeconds);
if(!saveTokenResult){
}
利用POST请求仿用户登录
问题:请求被spring security拦截了
web.ignoring().antMatchers("/userLogin","/userLogout","/userjwt"); //spring security放行请求
-
Header
包括令牌的类型+哈希算法
{ "alg": "HS256", "typ": "JWT" }
-
Payload负载
存放有效信息类容
{ "sub": "1234567890", "name": "456", "admin": true }
-
Signature签名
防止jwt内容被篡改该
使用Base64Url编码
header+"."+payload—>数字签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)//secret唯一标识
4.3 创建JWT令牌的准备工作
密钥证书:公钥+私钥
JDK生成密钥证书:
keytool -genkeypair -alias carterkey -keyalg RSA -keypass root -keystore carter.keystore -storepass rootstore
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码
- 查看证书信息
keytool -list -keystore carter.keystore
4.4 利用私钥生成令牌
利用证书—>私钥+内容—>jwt
4.5 利用公钥校验令牌
公钥错误—>生成JWT令牌错误(spring security已经做)
5、Redis存储JWT令牌
[外链图片转存中…(img-eakJQmpw-1604028272894)]
- 是否过期
//读取过期时间,已过期返回‐2
Long expire = stringRedisTemplate.getExpire(key);
6、spring security oauth2.0登录注册案例
6.1 什么时候发放令牌
[外链图片转存中…(img-vfLjLN8s-1604028272896)]
6.2 客户端id—参数放在哪里?
在application.yml中配置参数
auth:
tokenValiditySeconds: 1200 #token存储到redis的过期时间
clientId: XcWebApp #客户端id
clientSecret: XcWebApp #客户端密码
cookieDomain: localhost #域名
cookieMaxAge: ‐1 #浏览器关闭cookie无效
6.3 原远程生成令牌代码实现
【AUU认证服务】里面集成了spring security
- 远程请求 spring security 生成令牌
- 远程调用userLogin方法
userLogin方法用来申请令牌(账号+密码+客户端id)
- AUU服务的Controller层
/userLogin
参数:
账号username和密码password
client ID是yaml文件中的
作用: 将令牌存储到cookie
cookie:【response,域名,根路径,name:uid,token,cookie有效期,httpOnly:false浏览器可访问cookie】
域名:carter.com—指向—>localhost:8080 相当网关的反向代理 nginx技术
- Service层
客户端id—>yaml中存取
将令牌存到cookie中
//认证方法
public AuthToken login(String username,String password,String clientId,String clientSecret){
//申请令牌
AuthToken authToken = applyToken(username,password,clientId, clientSecret);
if(authToken == null){
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
}
//将 token存储到redis
String access_token = authToken.getAccess_token();
String content = JSON.toJSONString(authToken);
boolean saveTokenResult = saveToken(access_token, content, tokenValiditySeconds);
if(!saveTokenResult){
}
利用POST请求仿用户登录
问题:请求被spring security拦截了
web.ignoring().antMatchers("/userLogin","/userLogout","/userjwt"); //spring security放行请求