1.基本概念
1.1.什么是认证
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式。
1.2 什么是会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session
方式、基于token
方式等。
基于session
的认证方式如下图:
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的 sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数 据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于token
方式如下图:
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
总结:基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持 cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
1.2 什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
1.3 授权的数据模型
如何进行授权即如何对用户访问资源进行控制,首先需要学习授权相关的数据模型。
授权可简单理解为Who对What(which)进行How操作,包括如下:
Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。 What,即资源 (Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按 钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息 都属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号 为001 的商品为资源实例。 How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义, 如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户 对哪些资源都有哪些操作许可。
主体、资源、权限关系如下图:
主体、资源、权限相关的数据模型如下:
主体(用户id、账号、密码、…)
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
角色(角色id、角色名称、…)
角色和权限关系(角色id、权限id、…)
主体(用户)和角色关系(用户id、角色id、…)
主体(用户)、资源、权限关系如下图:
通常企业开发中将资源和权限表合并为一张权限表,如下:
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
合并为: 权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)
修改后数据模型之间的关系如下图:
1.4 RBAC
如何实现授权?业界通常基于RBAC实现授权。
1.4.1 基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:
根据上图中的判断逻辑,授权代码可表示如下:
if(主体.hasRole(“总经理角色id”)){
查询工资
}
如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是 总经理或部门经理”,修改代码如下:
if(主体.hasRole(“总经理角色id”) || 主体.hasRole(“部门经理角色id”)){
查询工资
}
根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。
1.4.2 基于资源(权限)的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须 有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
根据上图中的判断,授权代码可以表示为:
if(主体.hasPermission(“查询工资权限标识”)){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
2.基于Session的认证方式
2.1 认证流程
基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发 给客户端的 sesssion_id 存放到 cookie 中,这样用客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验。当用户退出系统或session过期销毁时,客户端的session_id也就无效了。 下图是session认证方式的流程图:
基于Session的认证机制由Servlet规范定制,Servlet容器已实现,用户通过HttpSession的操作方法即可实现,如 下是HttpSession相关的操作API。
方法 | 含义 |
---|---|
HttpSession getSession(Boolean create) | 获取当前HttpSession对象 |
void setAttribute(String name,Object value) | 向session中存放对象 |
object getAttribute(String name) | 从session中获取对象 |
void removeAttribute(String name); | 移除session中对象 |
void invalidate() | 使HttpSession失效 |
略… | … |
2.2.创建工程
本案例工程使用maven进行构建,使用SpringMVC、Servlet3.0实现。
2.2.1 创建maven工程
创建maven工程 security-springmvc,工程结构如下:
pom.xml
文件引入如下依赖如下,注意: 1、由于是web工程,packaging设置为war 2、使用tomcat7-maven-plugin插件来运行工程。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.king.security</groupId>
<artifactId>security-springmvc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<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</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>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
2.2.2 Spring 容器配置
在config
包下定义ApplicationConfig.java
,它对应web.xml
中ContextLoaderListener
的配置
package com.king.security.springmvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
/**
* 加载Spring容器,相当于applicationContext.xml
**/
@Configuration
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 排除Controller层不进行扫描,在WebConfig类进行扫描
public class ApplicationConfig {
//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、aop、业务bean等。
}
2.2.3 servletContext配置
本案例采用Servlet3.0
无web.xml
方式,的config
包下定义WebConfig.java
,它对应于DispatcherServlet
配置。
package com.king.security.springmvc.config;
import com.king.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* ServletContext,就相当于springmvc.xml文件
**/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {
@Autowired
SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
/**
* 视频解析器
* @return
*/
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// addViewController:URL访问路径
// setViewName:映射的页面
registry.addViewController("/").setViewName("login");
}
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addInterceptor:需要一个实现 HandlerInterceptor 接口的拦截器实例。
// addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截。
registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
// excludePathPatterns:用于设置不需要拦截的过滤规则。
registry.addInterceptor(simpleAuthenticationInterceptor).excludePathPatterns("/","/login","/js/**","/css/**","/images/**");
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("token")
.maxAge(3600);
}
}
2.2.4 加载 Spring容器
在init
包下定义Spring
容器初始化类SpringApplicationInitializer
,此类实现WebApplicationInitializer
接口, Spring容器启动时加载WebApplicationInitializer
接口的所有实现类。
package com.king.security.springmvc.init;
import com.king.security.springmvc.config.ApplicationConfig;
import com.king.security.springmvc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* 加载Spring容器以及初始化,就相当于web.xml,使用了servlet3.0开发则不需要再定义web.xml
**/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* spring容器,相当于加载 applicationContext.xml
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class};
}
/**
* servletContext,相当于加载springmvc.xml
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* url-mapping
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* web.xml参考:
* <web‐app>
* <listener>
* <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
* </listener>
* <context‐param>
* <param‐name>contextConfigLocation</param‐name>
* <param‐value>/WEB‐INF/application‐context.xml</param‐value>
* </context‐param>
* <servlet>
* <servlet‐name>springmvc</servlet‐name>
* <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
* <init‐param>
* <param‐name>contextConfigLocation</param‐name>
* <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
* </init‐param>
* <load‐on‐startup>1</load‐on‐startup>
* </servlet>
* <servlet‐mapping>
* <servlet‐name>springmvc</servlet‐name>
* <url‐pattern>/</url‐pattern>
* </servlet‐mapping>
* </web‐app>
*/
}
SpringApplicationInitializer
相当于web.xml
,使用了servlet3.0开
发则不需要再定义web.xml
, ApplicationConfig.class
对应以下配置的application-context.xml
,WebConfig.class
对应以下配置的spring- mvc.xml
,web.xml
的内容参考:
<web‐app>
<listener>
<listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
</listener>
<context‐param>
<param‐name>contextConfigLocation</param‐name>
<param‐value>/WEB‐INF/application‐context.xml</param‐value>
</context‐param>
<servlet>
<servlet‐name>springmvc</servlet‐name>
<servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
<init‐param>
<param‐name>contextConfigLocation</param‐name>
<param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
</init‐param>
<load‐on‐startup>1</load‐on‐startup>
</servlet>
<servlet‐mapping>
<servlet‐name>springmvc</servlet‐name>
<url‐pattern>/</url‐pattern>
</servlet‐mapping>
</web‐app>
2.3.实现认证功能
2.3.1 认证页面
在webapp/WEB-INF/views
下定义认证页面login.jsp
,本案例只是测试认证流程,页面没有添加css
样式,页面实现可填入用户名,密码,触发登录将提交表单信息至/login
,内容如下:
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
<title>用户登录</title>
</head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username"><br>
密 码:
<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
在WebConfig
中新增如下配置,将/直接导向login.jsp
页面:
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// addViewController:URL访问路径
// setViewName:映射的页面
registry.addViewController("/").setViewName("login");
}
启动项目,访问/路径地址,进行测试:
2.3.2 认证接口
用户进入认证页面,输入账号和密码,点击登录,请求/login
进行身份认证。
(1)定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:
package com.king.security.springmvc.service;
import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
/**
* 认证服务
*/
public interface AuthenticationService {
/**
* 用户认证
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
UserDto authentication(AuthenticationRequest authenticationRequest);
}
认证请求结构:
package com.king.security.springmvc.model;
import lombok.Data;
/**
*
**/
@Data
public class AuthenticationRequest {
//认证请求参数,账号、密码。。
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
认证成功后返回的用户详细信息,也就是当前登录用户的信息:
package com.king.security.springmvc.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Set;
/**
* 用户信息
**/
@Data
@AllArgsConstructor
public class UserDto {
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
(2)认证实现类,根据用户名查找用户信息,并校验密码,这里模拟了两个用户:
package com.king.security.springmvc.service.impl;
import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 认证服务实现类
**/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
/**
* 用户认证,校验用户身份信息是否合法
*
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
//校验参数是否为空
if(authenticationRequest == null
|| StringUtils.isEmpty(authenticationRequest.getUsername())
|| StringUtils.isEmpty(authenticationRequest.getPassword())){
throw new RuntimeException("账号和密码为空");
}
//根据账号去查询数据库,这里测试程序采用模拟方法
UserDto user = getUserDto(authenticationRequest.getUsername());
//判断用户是否为空
if(user == null){
throw new RuntimeException("查询不到该用户");
}
//校验密码
if(!authenticationRequest.getPassword().equals(user.getPassword())){
throw new RuntimeException("账号或密码错误");
}
//认证通过,返回用户身份信息
return user;
}
//根据账号查询用户信息
private UserDto getUserDto(String userName){
return userMap.get(userName);
}
/**
* 用于存储用户信息
*/
private Map<String,UserDto> userMap = new HashMap<>();
/**
* 构造代码块,初始化用户信息
*/
{
userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443"));
userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553"));
}
}
(3)登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息:
package com.king.security.springmvc.controller;
import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
* 登录控制层
**/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
System.out.println("session:"+session.getAttribute(UserDto.SESSION_USER_KEY));
return userDto.getUsername() +"登录成功";
}
}
(5)测试 启动项目,访问/路径地址,进行测试
填入错误的用户信息,页面返回错误信息:
填入正确的用户信息,页面提示登录成功:
以上的测试全部符合预期,到目前为止最基础的认证功能已经完成,它仅仅实现了对用户身份凭证的校验,若某用户认证成功,只能说明他是该系统的一个合法用户,仅此而已。
2.4.实现会话功能
会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。
认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性 拦截。因此,在认证成功后,一般会把认证成功的用户信息放入Session中,在后续的请求中,系统能够从Session 中获取到当前用户,用这样的方式来实现会话机制。
(1)增加会话控制
首先在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。
public static final String SESSION_USER_KEY = "_user";
然后修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效。
package com.king.security.springmvc.controller;
import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
* 登录控制层
**/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
System.out.println("session:"+session.getAttribute(UserDto.SESSION_USER_KEY));
return userDto.getUsername() +"登录成功";
}
@GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "退出成功";
}
}
(2)增加测试资源
修改LoginController
,增加测试资源/r/r1
,它从当前会话session
中获取当前登录用户,并返回提示信息给前台。
package com.king.security.springmvc.controller;
import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
* 登录控制层
**/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
System.out.println("session:"+session.getAttribute(UserDto.SESSION_USER_KEY));
return userDto.getUsername() +"登录成功";
}
@GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "退出成功";
}
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(HttpSession session){
String fullname = null;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
fullname = "匿名";
}else{
UserDto userDto = (UserDto) object;
fullname = userDto.getFullname();
}
return fullname+"访问资源r1";
}
}
(3)测试
未登录情况下直接访问测试资源/r/r1:
成功登录的情况下访问测试资源/r/r1:
测试结果说明,在用户登录成功时,该用户信息已被成功放入session
,并且后续请求可以正常从session
中获取当 前登录用户信息,符合预期结果。
2.5.实现授权功能
现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:
- 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
- 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。
(1)增加权限数据为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto的构造方法。
package com.king.security.springmvc.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Set;
/**
* 用户信息
**/
@Data
@AllArgsConstructor
public class UserDto {
public static final String SESSION_USER_KEY = "_user";
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
/**
* 用户权限
*/
private Set<String> authorities;
}
并在AuthenticationServiceImpl
中为模拟用户初始化权限,其中张三给了p1权限
,李四给了p2权限
。
package com.king.security.springmvc.service.impl;
import com.king.security.springmvc.model.AuthenticationRequest;
import com.king.security.springmvc.model.UserDto;
import com.king.security.springmvc.service.AuthenticationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 认证服务实现类
**/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
/**
* 用户认证,校验用户身份信息是否合法
*
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
//校验参数是否为空
if(authenticationRequest == null
|| StringUtils.isEmpty(authenticationRequest.getUsername())
|| StringUtils.isEmpty(authenticationRequest.getPassword())){
throw new RuntimeException("账号和密码为空");
}
//根据账号去查询数据库,这里测试程序采用模拟方法
UserDto user = getUserDto(authenticationRequest.getUsername());
//判断用户是否为空
if(user == null){
throw new RuntimeException("查询不到该用户");
}
//校验密码
if(!authenticationRequest.getPassword().equals(user.getPassword())){
throw new RuntimeException("账号或密码错误");
}
//认证通过,返回用户身份信息
return user;
}
//根据账号查询用户信息
private UserDto getUserDto(String userName){
return userMap.get(userName);
}
/**
* 用于存储用户信息
*/
private Map<String,UserDto> userMap = new HashMap<>();
/**
* 构造代码块,初始化用户信息
*/
{
Set<String> authorities1 = new HashSet<>();
authorities1.add("p1");//这个p1我们人为让它和/r/r1对应
Set<String> authorities2 = new HashSet<>();
authorities2.add("p2");//这个p2我们人为让它和/r/r2对应
userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
}
}
(3)实现授权拦截器
在interceptor
包下定义SimpleAuthenticationInterceptor
拦截器,实现授权拦截:
1、校验用户是否登录
2、校验用户是否拥有操作权限
package com.king.security.springmvc.interceptor;
import com.king.security.springmvc.model.UserDto;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 自定义拦截器
**/
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
/**
* 前置拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在这个方法中校验用户请求的url是否在用户的权限范围内
//取出用户身份信息
Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
//没有认证,提示登录
writeContent(response,"请登录");
}
UserDto userDto = (UserDto) object;
//请求的url
String requestURI = request.getRequestURI();
if( (userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")) || (userDto.getAuthorities().contains("p1") && requestURI.contains("/logout"))){
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();
}
}
在WebConfig
中配置拦截器,匹配/r/**
的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor
拦截器。
package com.king.security.springmvc.config;
import com.king.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* ServletContext,就相当于springmvc.xml文件
**/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {
@Autowired
SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
/**
* 视频解析器
* @return
*/
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// addViewController:URL访问路径
// setViewName:映射的页面名称
registry.addViewController("/").setViewName("login");
}
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addInterceptor:需要一个实现 HandlerInterceptor 接口的拦截器实例。
// addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截。
registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
// excludePathPatterns:用于设置不需要拦截的过滤规则。
registry.addInterceptor(simpleAuthenticationInterceptor).excludePathPatterns("/","/login","/js/**","/css/**","/images/**");
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("token")
.maxAge(3600);
}
}
(4)测试
未登录情况下,/r/r1与/r/r2均提示 “请先登录”。
张三登录情况下,由于张三有p1权限,因此可以访问/r/r1,张三没有p2权限,访问/r/r2时提示 “权限不足 “。
李四登录情况下,由于李四有p2权限,因此可以访问/r/r2,李四没有p1权限,访问/r/r1时提示 “权限不足 “。
测试结果全部符合预期结果。
2.6.小结
基于Session的认证方式是一种常见的认证方式,至今还有非常多的系统在使用。我们在此小节使用Spring mvc技 术对它进行简单实现,旨在让大家更清晰实在的了解用户认证、授权以及会话的功能意义及实现套路,也就是它们 分别干了哪些事儿?大概需要怎么做?
而在正式生产项目中,我们往往会考虑使用第三方安全框架(如 spring security,shiro等安全框架)来实现认证 授权功能,因为这样做能一定程度提高生产力,提高软件标准化程度,另外往往这些框架的可扩展性考虑的非常全 面。但是缺点也非常明显,这些通用化组件为了提高支持范围会增加很多可能我们不需要的功能,结构上也会比较 抽象,如果我们不够了解它,一旦出现问题,将会很难定位。
3.Spring Security快速上手
3.1 Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。
3.2 创建工程
3.2.1 创建maven工程
创建maven工程 security-springsecurity,工程结构如下:
2)引入以下依赖:
在security-springmvc
的项目基础上增加spring-security
的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.king.security</groupId>
<artifactId>security-springsecurity</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<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>
<!-- 引入SpringSecurity依赖 -->
<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-springsecurity</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>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
3.2.2 Spring容器配置
同security-springmvc项目一样.
package com.king.security.springmvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
/**
* 加载Spring容器,相当于applicationContext.xml
**/
@Configuration
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 排除Controller层不进行扫描,在WebConfig类进行扫描
public class ApplicationConfig {
//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、aop、业务bean等。
}
3.2.3 Servlet Context配置
同security-springmvc项目一样.
package com.king.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* ServletContext,就相当于springmvc.xml文件
**/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {
/**
* 视频解析器
* @return
*/
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// addViewController:URL访问路径
// setViewName:映射的页面名称
registry.addViewController("/").setViewName("login");
}
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("token")
.maxAge(3600);
}
}
3.2.4 加载 Spring容器
在init
包下定义Spring
容器初始化类SpringApplicationInitialize
r,此类实现WebApplicationInitializer
接口,Spring容器启动时加载WebApplicationInitializer
接口的所有实现类。
package com.king.security.springmvc.init;
import com.king.security.springmvc.config.ApplicationConfig;
import com.king.security.springmvc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* 加载Spring容器以及初始化,就相当于web.xml,使用了servlet3.0开发则不需要再定义web.xml
**/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* spring容器,相当于加载 applicationContext.xml
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class};
}
/**
* servletContext,相当于加载springmvc.xml
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* url-mapping
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* web.xml参考:
* <web‐app>
* <listener>
* <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
* </listener>
* <context‐param>
* <param‐name>contextConfigLocation</param‐name>
* <param‐value>/WEB‐INF/application‐context.xml</param‐value>
* </context‐param>
* <servlet>
* <servlet‐name>springmvc</servlet‐name>
* <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
* <init‐param>
* <param‐name>contextConfigLocation</param‐name>
* <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
* </init‐param>
* <load‐on‐startup>1</load‐on‐startup>
* </servlet>
* <servlet‐mapping>
* <servlet‐name>springmvc</servlet‐name>
* <url‐pattern>/</url‐pattern>
* </servlet‐mapping>
* </web‐app>
*/
}
3.3 认证
3.3.1 认证页面
spring security
默认提供认证页面,不需要额外开发。
3.3.2.安全配置
spring security
提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
1)在config
包下定义WebSecurityConfig
,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
package com.king.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* spring security安全配置
**/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息认证服务(认证)
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 添加认证放行的用户信息并设置权限
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1","p3").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
/**
* 密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
return NoOpPasswordEncoder.getInstance();
}
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
}
在userDetailsService()
方法中,我们返回了一个UserDetailsService
给spring容器,Spring Security
会使用它来获取用户信息。我们暂时使用InMemoryUserDetailsManager
实现类,并在其中分别创建了zhangsan
、lisi
两个用户,并设置密码和权限。
而在configure()
安全拦截机制方法中,我们通过HttpSecurity设置了安全拦截规则,其中包含了以下内容:
(1)url匹配/r/**的资源,经过认证后才能访问。
(2)其他url完全开放。
(3)支持form表单认证,认证成功后转向/login-success。
关于HttpSecurity的配置清单请参考附录 HttpSecurity。
2)让 Spring
容器加载 WebSecurityConfig
配置
修改SpringApplicationInitializer
类的getRootConfigClasses()
方法,添加WebSecurityConfig.class
:
package com.king.security.springmvc.init;
import com.king.security.springmvc.config.ApplicationConfig;
import com.king.security.springmvc.config.WebConfig;
import com.king.security.springmvc.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* 加载Spring容器以及初始化,就相当于web.xml,使用了servlet3.0开发则不需要再定义web.xml
**/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* spring容器,相当于加载 applicationContext.xml
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
// 加载ApplicationConfig类、WebSecurityConfig类
return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
}
/**
* servletContext,相当于加载springmvc.xml
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* url-mapping
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* web.xml参考:
* <web‐app>
* <listener>
* <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
* </listener>
* <context‐param>
* <param‐name>contextConfigLocation</param‐name>
* <param‐value>/WEB‐INF/application‐context.xml</param‐value>
* </context‐param>
* <servlet>
* <servlet‐name>springmvc</servlet‐name>
* <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
* <init‐param>
* <param‐name>contextConfigLocation</param‐name>
* <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
* </init‐param>
* <load‐on‐startup>1</load‐on‐startup>
* </servlet>
* <servlet‐mapping>
* <servlet‐name>springmvc</servlet‐name>
* <url‐pattern>/</url‐pattern>
* </servlet‐mapping>
* </web‐app>
*/
}
3.3.2.Spring Security初始化
Spring Security
初始化,这里有两种情况
- 若当前环境没有使用
Spring
或Spring MVC
,则需要将WebSecurityConfig(Spring Security配置类)
传入超类,以确保获取配置,并创建spring context
。 - 相反,若当前环境已经使用
spring
,我们应该在现有的springContext
中注册Spring Security(上一步已经做将 WebSecurityConfig 加载至 rootcontext)
,此方法可以什么都不做。 在init
包下定义SpringSecurityApplicationInitializer
:
package com.king.security.springmvc.init;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
/**
* 初始化Spring Security配置
**/
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
public SpringSecurityApplicationInitializer() {
/**
* 如果当前环境下没有使用 Spring 或 SpringMVC,则需要将 WebSecurityConfig(Spring Security配置类) 传入超类,以确保获取配置,并创建spring context。
* 相反,若当前环境已经使用spring,应该在现有的springContext中注册Spring Security(上一步已经做将WebSecurityConfig加载至RootConfigClasses),此方法可以什么都不做。
*/
//super(WebSecurityConfig.class);
}
}
3.2.3.默认根路径请求
在WebConfig.java
中添加默认请求根路径跳转到/login
,此url为spring security
提供:
package com.king.security.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* ServletContext,就相当于springmvc.xml文件
**/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.king.security.springmvc" // 指定扫描包层级
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) // 扫描Controller层
public class WebConfig implements WebMvcConfigurer {
/**
* 视频解析器
* @return
*/
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 默认Url根路径跳转到/login,此url为spring security提供的登录页
// addViewController:URL访问路径
// setViewName:映射的页面名称
// 重定向到/login页面去
registry.addViewController("/").setViewName("redirect:/login");
}
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("token")
.maxAge(3600);
}
}
spring security
默认提供的登录页面。
3.2.4.认证成功页面
在安全配置中,认证成功将跳转到/login-success
,代码如下:
/**
* 安全拦截机制(最重要)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
spring security
支持form
表单认证,认证成功后转向/login-success
。 在LoginController
中定义/login-success
:
package com.king.security.springmvc.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录控制层
**/
@RestController
public class LoginController {
/**
* 认证成功将跳转到/login-success
* @return
*/
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
return " 登录成功";
}
}
3.2.5 测试
(1)启动项目,访问 http://localhost:8080/security-springsecurity/login
路径地址:
页面会根据WebConfig
中addViewControllers
配置规则,跳转至/login
,/login
是Spring Security
提供的登录页面。
(2)登录
1、输入错误的用户名、密码
2、输入正确的用户名、密码,登录成功
(3)退出
1、请求 http://localhost:8080/security-springsecurity/logout
退出
2、退出后再访问资源自动跳转到登录页面
3.4 授权
实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security
默认提供授权实现方法。
在 LoginController
类添加 /r/r1
方法或 /r/r2
方法:
/**
* 测试资源1
* @return
*/
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
return " 访问资源1";
}
/**
* 测试资源2
* @return
*/
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
return " 访问资源2";
}
在安全配置类 WebSecurityConfig.java
中配置授权规则:
.antMatchers("/r/r1").hasAuthority(“p1”)
.antMatchers("/r/r2").hasAuthority(“p2”)
.antMatchers("/r/r1").hasAuthority("p1")
表示:访问/r/r1资源的 url需要拥有p1权限。
.antMatchers("/r/r2").hasAuthority("p2")
表示:访问/r/r2资源的 url需要拥有p2权限。
完整的WebSecurityConfig
配置类的 configure()
方法如下:
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
测试:
1、登录成功
2、访问/r/r1和/r/r2,有权限时则正常访问,否则返回403(拒绝访问)
3.5 小结
通过快速上手,咱们使用Spring Security
实现了认证和授权,Spring Security
提供了基于账号和密码的认证方式,通过安全配置即可实现请求拦截,授权功能,Spring Security
能完成的不仅仅是这些。
4.Spring Security 应用详解
4.1 集成SpringBoot
4.1.1 Spring Boot 介绍
Spring Boot
是一套Spring
的快速开发框架,基于Spring 4.0
设计,使用Spring Boot
开发可以避免一些繁琐的工程搭建和配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 Spring Boot
开发,例如:MyBatis
、Dubbo
等,Spring
家族更是如此,例如:Spring cloud
、Spring mvc
、Spring security
等,使用Spring Boot
开发可以大大得高生产率,所以Spring Boot
的使用率非常高。
本章节讲解如何通过Spring Boot
开发Spring Security
应用,Spring Boot
提供spring-boot-starter-security
用于开发Spring Security
应用。
4.1.2 创建maven工程
1)创建maven
工程 security-springboot
,工程结构如下:
2)引入以下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.king.security</groupId>
<artifactId>security-springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-springboot</name>
<description>security-springboot</description>
<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>
<!-- 以下是>spring boot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 以下是>spring security依赖-->
<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>
<!-- 用于编译jsp -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</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>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
4.1.3 spring 容器配置
SpringBoot
工程启动会自动扫描启动类所在包下的所有 Bean
,加载到 spring
容器。
1)Spring Boot配置文件
在resources
下添加application.properties
,内容如下:
# 端口号
server.port=8080
# 默认的URL上下文路径
server.servlet.context-path=/security-springboot
# 应用程序名称
spring.application.name=security-springboot
# 页面映射
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp
# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=rootroot
2)Spring Boot 启动类
package com.king.security.securityspringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecuritySpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritySpringbootApplication.class, args);
}
}
4.1.4 Servlet Context配置
由于Spring boot starter
自动装配机制,这里无需使用@EnableWebMvc
与@ComponentScan
,WebConfig
配置文件如下:
package com.king.security.securityspringboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
/**
* ServletContext,就相当于springmvc.xml文件
**/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 默认Url根路径跳转到/login,此url为spring security提供的登录页
// addViewController:URL访问路径
// setViewName:映射的页面名称
// 重定向到/login页面去
registry.addViewController("/").setViewName("redirect:/login");
}
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("token")
.maxAge(3600);
}
}
视图解析器配置在 application.properties
文件中:
# 端口号
server.port=8080
# 默认的URL上下文路径
server.servlet.context-path=/security-springboot
# 应用程序名称
spring.application.name=security-springboot
# 页面映射
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp
# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=rootroot
4.1.5 安全配置
由于 Spring boot starter
自动装配机制,这里无需使用@EnableWebSecurity
,WebSecurityConfig
配置文件内容如下:
package com.king.security.securityspringboot.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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* spring security安全配置
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息认证服务(认证)
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 添加认证放行的用户信息并设置权限
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
/**
* 校验密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
return NoOpPasswordEncoder.getInstance();
}
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
}
4.1.6 测试
LoginController
类的内容:
package com.king.security.securityspringboot.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录控制层
**/
@RestController
public class LoginController {
/**
* 认证成功将跳转到/login-success
* @return
*/
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
return " 登录成功";
}
/**
* 测试资源1
* @return
*/
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
return " 访问资源1";
}
/**
* 测试资源2
* @return
*/
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
return " 访问资源2";
}
}
测试过程:
1、测试认证
2、测试退出
3、测试授权
4.2 工作原理
4.2.1 结构总览
Spring Security
所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过 Filter
或 AOP
等技术来实现,Spring Security
对 Web
资源的保护是靠 Filter
实现的,所以从这个 Filter
来入手,逐步深入 Spring Security
原理。
当初始化 Spring Security
时,会创建一个名为 SpringSecurityFilterChain
的 Servlet过滤器
,类型为 org.springframework.security.web.FilterChainProxy
,它实现javax.servlet.Filter
,因此外部的请求会经过此 类,下图是 Spring Security
过虑器链结构图:
FilterChainProxy
是一个代理,真正起作用的是 FilterChainProxy
中 SecurityFilterChain
所包含的各个Filter
,同时这些 Filter
作为 Bean
被 Spring
管理,它们是 Spring Security
核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)
和决策管理器 (AccessDecisionManager)
进行处理,下图是 FilterChainProxy
相关类的 UML
图示。
Spring Security
功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter
这个 Filter
是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository
中获取 SecurityContext
,然后把它设置给 SecurityContextHolder
。在请求完成后将 SecurityContextHolder
持有的 SecurityContext
再保存到配置好的 SecurityContextRepository
,同时清除 SecurityContextHolder
所持有的SecurityContext;
UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler
和 AuthenticationFailureHandler
,这些都可以根据需求做相关改变;
FilterSecurityInterceptor
是用于保护 web
资源的,使用 AccessDecisionManager
对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter
能够捕获来自 FilterChain
所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException
和 AccessDeniedException
,其它的异常它会继续抛出。
4.2.2.认证流程
4.2.2.1 认证流程
让我们仔细分析认证过程:
- 用户提交用户名、密码被
SecurityFilterChain
中的UsernamePasswordAuthenticationFilter
过滤器获取到,封装为请求Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类。 - 然后过滤器将
Authentication
提交至认证管理器(AuthenticationManager)
进行认证。 - 认证成功后,
AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除)Authentication
实例。 SecurityContextHolder
安全上下文容器将第3步
填充了信息的Authentication
,通过SecurityContextHolder.getContext().setAuthentication(...)
方法,设置到其中。可以看出AuthenticationManager
接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager
。而Spring Security
支持多种认证方式,因此ProviderManager
维护着一个List<AuthenticationProvider>
列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider
完成的。咱们知道web
表单的对应的AuthenticationProvider
实现类为DaoAuthenticationProvider
,它的内部又维护着一个UserDetailsService
负责UserDetails
的获取。最终AuthenticationProvider
将UserDetails
填充至Authentication
。
认证核心组件的大体关系如下:
4.3.2.2.AuthenticationProvider
通过前面的 Spring Security
认证流程我们得知,认证管理器(AuthenticationManager)
委托 AuthenticationProvider
完成认证工作。
AuthenticationProvider
是一个接口,定义如下:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> var1);
}
authenticate()
方法定义了认证的实现过程,它的参数是一个Authentication
,里面包含了登录用户所提交的用 户、密码等。而返回值也是一个 Authentication
,这个 Authentication
则是在认证成功后,将用户的权限及其他信息重新组装后生成。
Spring Security
中维护着一个 List<AuthenticationProvider>
列表,存放多种认证方式,不同的认证方式使用不同的 AuthenticationProvider
。如使用用户名密码登录时,使用 AuthenticationProvider1
,短信登录时使用AuthenticationProvider2
等等这样的例子很多。
每个 AuthenticationProvider
需要实现 supports()
方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时 Spring Security
会生成UsernamePasswordAuthenticationToken
,它是一个 Authentication
,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider
来处理它?
在 DaoAuthenticationProvider
的基类AbstractUserDetailsAuthenticationProvider
发现以下代码:
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
也就是说当 web
表单提交用户名密码时,Spring Security
由DaoAuthenticationProvider
处理。
最后,来看一下 Authentication(认证信息)
的结构,它是一个接口,我们之前提到的 UsernamePasswordAuthenticationToken
就是它的实现之一:
public interface Authentication extends Principal, Serializable { (1)
Collection<? extends GrantedAuthority> getAuthorities(); (2)
Object getCredentials(); (3)
Object getDetails(); (4)
Object getPrincipal(); (5)
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
(1) Authentication
是 spring security
包中的接口,直接继承自 Principal
类,而 Principal
是位于 java.security
包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个 getName()
方法。
(2) getAuthorities()
是获取权限信息列表,默认是 GrantedAuthority
接口的一些实现类,通常是代表权限信息的一系列字符串。
(3) getCredentials()
凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
(4) getDetails()
细节信息,web
应用中的实现接口通常为WebAuthenticationDetails
,它记录了访问者的 ip
地址和 sessionId
的值。
(5) getPrincipal()
,身份信息,大部分情况下返回的是 UserDetails
接口的实现类,UserDetails
代表用户的详细信息,那从 Authentication
中取出来的 UserDetails
就是当前登录用户信息,它也是框架中的常用接口之一。
4.3.2.3.UserDetailsService
1)认识UserDetailsService
现在咱们现在知道 DaoAuthenticationProvider
处理了 web
表单的认证逻辑,认证成功后既得到一个 Authentication(UsernamePasswordAuthenticationToken实现)
,里面包含了身份信息(Principal
)。这个身份 信息就是一个 Object
,大多数情况下它可以被强转为 UserDetails
对象。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
很多人把 DaoAuthenticationProvider
和 UserDetailsService
的职责搞混淆,其实 UserDetailsService
只负责从特定 的地方(通常是数据库)加载用户信息,仅此而已。而 DaoAuthenticationProvider
的职责更大,它完成完整的认证流程,同时会把 UserDetails
填充至 Authentication
。
上面一直提到 UserDetails
是用户信息,咱们看一下它的真面目:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和 Authentication
接口很类似,比如它们都拥有username
,authorities
。Authentication
的 getCredentials()
与 UserDetails
中的 getPassword()
需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication
中的 getAuthorities()
实际是由UserDetails的getAuthorities()
传递而形成的。还记得 Authentication
接口中的getDetails()
方法吗?其中的 UserDetails
用户详细信息便是经过了 AuthenticationProvider
认证之后被填充的。
通过实现 UserDetailsService
和 UserDetails
,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security
提供的 InMemoryUserDetailsManager(内存认证)
,JdbcUserDetailsManager(jdbc认证)
就是 UserDetailsService
的实现类,主要区别无非就是从内存还是从数据库加载用户。
2)测试
自定义UserDetailsService
package com.king.security.securityspringboot.service;
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;
/**
* 自定UserDetails用户认证器
*/
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
/**
* 测试此方法要把com.king.security.securityspringboot.config.userDetailsService()注释掉
* 根据用户账号查询信息
* @param username 用户账号
* @return 返回用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//登录账号
System.out.println("username="+username);
//根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
return userDetails;
}
}
屏蔽 WebSecurityConfig
安全配置类中 UserDetailsService()
方法的定义:
/**
* spring security安全配置
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息认证服务(认证)
* @return
*/
// @Bean
// public UserDetailsService userDetailsService(){
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// // 添加认证放行的用户信息并设置权限
// manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
// manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
// return manager;
// }
/**
* 密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
return NoOpPasswordEncoder.getInstance();
}
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
}
重启工程,请求认证,SpringDataUserDetailsService
的loadUserByUsername
方法被调用 ,查询用户信息。
4.3.2.4.PasswordEncoder
1)认识PasswordEncoder
DaoAuthenticationProvider
认证处理器通过 UserDetailsService
获取到UserDetails
后,它是如何与请求 Authentication
中的密码做对比呢?
在这里 Spring Security
为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider
通过 PasswordEncoder
接口的 matches
方法进行密码的对比,而具体的密码对比细节取决于实现:
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
而 Spring Security
提供很多内置的 PasswordEncoder
,能够开箱即用,使用某种 PasswordEncoder
只需要进行如下声明即可,如下:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
NoOpPasswordEncoder
采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
- 用户输入密码(明文 )
DaoAuthenticationProvider
获取UserDetails
(其中存储了用户的正确密码)DaoAuthenticationProvider
使用PasswordEncoder
对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncoder
的校验规则拿输入的密码和 UserDetails
中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败。
实际项目中推荐使用 BCryptPasswordEncoder
, Pbkdf2PasswordEncoder
, SCryptPasswordEncoder
等,感兴趣的大家可以看看这些 PasswordEncoder
的具体实现。
2)使用BCryptPasswordEncoder
1、配置 BCryptPasswordEncoder
在安全配置类中定义:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
测试发现认证失败,提示:Encoded password does not look like BCrypt
。
原因:
由于 UserDetails
中存储的是原始密码(比如:123),它不是 BCrypt
格式。
跟踪 DaoAuthenticationProvider
第33
行代码查看 userDetails
中的内容 ,跟踪第38
行代码查看 PasswordEncoder
的类型。
2、测试BCrypt
通过下边的代码测试 BCrypt
加密及校验的方法
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐test</artifactId>
<scope>test</scope>
</dependency>
编写测试方法:
package com.king.security.securityspringboot.testbcrypt;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
public class TestBCrypt {
@Test
public void test1() {
// 使用BCrypt给密码加密,第一个参数:原始密码;第二个参数:加盐,BCrypt.gensalt():自动生成盐,每次生成都不一样
String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());
System.out.println("------------------使用BCrypt加密后的密码:" + hashpw);
// 校验密码,第一个参数:明文密码;第二个参数:明文密码使用BCrypt方式加密后的密码
boolean checkpw = BCrypt.checkpw("123","$2a$10$IPjZxNeVstgKQh0P2X9DX.IXaMQrSFiDQg0wr8DVffqyAK3.bwggy");
boolean checkpw2 = BCrypt.checkpw("123","$2a$10$DRhG8b6Sv8oQpBaYuOM.UO83EP8XOaJNFMsnQ69n3J4jT5zB8twZm");
System.out.println(checkpw); // true
System.out.println(checkpw2); // true
}
}
3、修改安全配置类
将 UserDetailsService()
方法中的原始密码修改为 BCrypt
格式:
package com.king.security.securityspringboot.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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* spring security安全配置
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息认证服务(认证)
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 添加认证放行的用户信息并设置权限
manager.createUser(User.withUsername("zhangsan").password("$2a$10$IPjZxNeVstgKQh0P2X9DX.IXaMQrSFiDQg0wr8DVffqyAK3.bwggy").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
/**
* 检验密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
// return NoOpPasswordEncoder.getInstance();
// 使用BCrypt方式进行匹配密码
return new BCryptPasswordEncoder();
}
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
}
实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。
4.2.3.授权流程
4.2.3.1 授权流程
通过快速上手我们知道,Spring Security
可以通过 http.authorizeRequests()
对 web
请求进行授权保护。Spring Security
使用 标准Filter
建立了对 web
请求的拦截,最终实现对资源的授权访问。
Spring Security
的授权流程如下:
分析授权流程:
拦截请求
,已认证用户访问受保护的web
资源将被SecurityFilterChain
中的FilterSecurityInterceptor
的子类拦截。- 获取资源访问策略,
FilterSecurityInterceptor
会从SecurityMetadataSource
的子类DefaultFilterInvocationSecurityMetadataSource
获取要访问当前资源所需要的权限Collection<ConfigAttribute>
。
SecurityMetadataSource
其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...
- 最后,
FilterSecurityInterceptor
会调用AccessDecisionManager
进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
AccessDecisionManager(访问决策管理器)
的核心接口如下:
public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication authentication, Object object, Collection<ConfigAttribute>
configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; //略..
}
这里着重说明一下 decide
的参数:
authentication
: 要访问资源的访问者的身份。
object
: 要访问的受保护资源,web
请求对应 FilterInvocation
。
configAttributes
: 是受保护资源的访问策略,通过 SecurityMetadataSource
获取。
decide
接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
4.2.3.2 授权决策
AccessDecisionManager
采用投票的方式来确定是否能够访问受保护资源。
通过上图可以看出,AccessDecisionManager
中包含的一系列 AccessDecisionVoter
将会被用来对Authentication
是否有权访问受保护对象进行投票,AccessDecisionManager
根据投票结果,做出最终决策。
AccessDecisionVoter
是一个接口,其中定义有三个方法,具体结构如下所示:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = ‐1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}
vote()
方法的返回结果会是 AccessDecisionVoter
中定义的三个常量之一。ACCESS_GRANTED
表示同意, ACCESS_DENIED
表示拒绝,ACCESS_ABSTAIN
表示弃权。如果一个AccessDecisionVoter
不能判定当前 Authentication
是否拥有访问对应受保护对象的权限,则其vote()
方法的返回值应当为弃权 ACCESS_ABSTAIN
。
Spring Security
内置了三个基于投票的 AccessDecisionManager
实现类如下,它们分别是 AffirmativeBased
、ConsensusBased
和 UnanimousBased
。
AffirmativeBased
的逻辑是:
(1)只要有 AccessDecisionVoter
的投票为 ACCESS_GRANTED
则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出 AccessDeniedException
。
Spring Security
默认使用的是 AffirmativeBased
。
ConsensusBased
的逻辑是:
(1)如果赞成票多于反对票则表示通过;
(2)反过来,如果反对票多于赞成票则将抛出 AccessDeniedException
;
(3)如果赞成票与反对票相同且不等于 0
,并且属性 allowIfEqualGrantedDeniedDecisions
的值为 true
,则表示通过,否则将抛出异常 AccessDeniedException
。参数 allowIfEqualGrantedDeniedDecisions
的值默认为 true
;
(4)如果所有的 AccessDecisionVoter
都弃权了,则将视参数 allowIfAllAbstainDecisions
的值而定,如果该值为 true
则表示通过,否则将抛出异常 AccessDeniedException
。参数 allowIfAllAbstainDecisions
的值默认为 false
。
UnanimousBased
的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给 AccessDecisionVoter
进行投票,而 UnanimousBased
会一次只传递一个 ConfigAttribute
给 AccessDecisionVoter
进行投票。这也就意味着如果我们的AccessDecisionVoter
的逻辑是只要传递进来的 ConfigAttribute
中有一个能够匹配则投赞成票,但是放到 UnanimousBased
中其投票结果就不一定是赞成了。 UnanimousBased
的逻辑具体来说是这样的:
(1)如果受保护对象配置的某一个 ConfigAttribute
被任意的 AccessDecisionVoter
反对了,则将抛出 AccessDeniedException
;
(2)如果没有反对票,但是有赞成票,则表示通过;
(3)如果全部弃权了,则将视参数 allowIfAllAbstainDecisions
的值而定,true
则通过,false
则抛出 AccessDeniedException
。
Spring Security
也内置一些投票者实现类如 RoleVoter
、AuthenticatedVoter
和WebExpressionVoter
等,可以自行查阅资料进行学习。
4.3 自定义认证
Spring Security
提供了非常好的认证扩展方法,比如:快速上手中将用户信息存储到内存中,实际开发中用户信息通常在数据库,Spring security
可以实现从数据库读取用户信息,Spring security
还支持多种授权方法。
4.3.1 自定义登录页面
在 Spring Security 快速上手
项目中,你可能会想知道登录页面从哪里来的?因为我们并没有提供任何的 HTML
或 JSP
文件。Spring Security
的默认配置没有明确设定一个登录页面的 URL
,因此Spring Security
会根据启用的功能自动生成一个登录页面 URL
,并使用默认 URL
处理登录的提交内容,登录后跳转的到默认 URL
等等。尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望定义自己的登录页面。
4.3.1.1 认证页面
将 security-springmvc工程
的 login.jsp
拷贝到 security-springboot工程
下,目录保持一致。
4.3.1.2 配置认证页面
在 WebConfig.java
中配置认证页面地址:
package com.king.security.securityspringboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
/**
* ServletContext,就相当于springmvc.xml文件
**/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置页面映射列表
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 默认Url根路径跳转到/login,此url为spring security提供的登录页
// addViewController:URL访问路径
// setViewName:映射的页面名称
// 重定向到Spring Security自带的/login页面去
// registry.addViewController("/").setViewName("redirect:/login");
// 转向到自定义login页面去
registry.addViewController("/").setViewName("redirect:/loginView");
registry.addViewController("/loginView").setViewName("login");
}
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 这里使用SpringSecurity做认证、授权、拦截功能,就不用自定义拦截器了
}
/**
* 配置跨域
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 浏览器允许所有的域访问 / 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GWT","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("token")
.maxAge(3600);
}
}
4.3.1.3 修改Spring Security安全配置文件
在 WebSecurityConfig
文件中配置表单登录信息:
package com.king.security.securityspringboot.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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* spring security安全配置
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息认证服务(认证)
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 添加认证放行的用户信息并设置权限
manager.createUser(User.withUsername("zhangsan").password("$2a$10$IPjZxNeVstgKQh0P2X9DX.IXaMQrSFiDQg0wr8DVffqyAK3.bwggy").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
/**
* 检验密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
// return NoOpPasswordEncoder.getInstance();
// 使用BCrypt方式进行匹配密码
return new BCryptPasswordEncoder();
}
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录 (1)
.loginPage("/loginView") // 登录页请求路径 (2)
.loginProcessingUrl("/login") // 指定登录处理的URL,也就是用户名、密码表单提交的目的路径 (3)
.successForwardUrl("/login-success");//自定义登录成功的页面地址 (4)
}
}
(1)允许表单登录;
(2)指定我们自己的登录页,spring security
以重定向方式跳转到 /loginView
URL ,这个URL 映射登录页;
(3)指定登录处理的 URL
,也就是用户名、密码表单提交的目的路径;
(4)指定登录成功后的跳转 URL
;
(5)我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个 formLogin().permitAll()
方法允许任意用户访问基于表单登录的所有的 URL
。
4.3.1.4 测试
当用户没有认证时访问系统的资源会重定向到 login
页面:
输入账号和密码,点击登录,报错:
问题解决:
spring security
为防止 CSRF(Cross-site request forgery跨站请求伪造)
的发生,限制了除了 get
以外的大多数方法。
解决方法1:
屏蔽 CSRF
控制,即 spring security
不再限制 CSRF
。
修改配置 WebSecurityConfig
文件:
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // csrf().disable():屏蔽CSRF控制,即spring security不再限制CSRF
.authorizeRequests() //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/r/r2").hasAuthority("p2") // 有p2权限才能访问
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.loginPage("/loginView") // 登录页请求路径
.loginProcessingUrl("/login") // 指定登录处理的URL,也就是用户名、密码表单提交的目的路径
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
本案例采用方法1。
解决方法2:
在 login.jsp
页面添加一个 token
,spring security
会验证 token
,如果 token
合法则可以继续请求。
修改 login.jsp
文件:
<form action="login" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
...
</form>
4.3.2 连接数据库认证
前面的例子是将用户信息存储在内存中,实际项目中用户信息存储在数据库中,现在实现从数据库读取用户信息。根据前边对认证流程研究,只需要重新定义 UserDetailService
即可实现根据用户账号查询数据库。
4.3.2.1 创建数据库
创建 user_db
数据库:
CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
创建 t_user
表:
CREATE TABLE `t_user` (
`id` BIGINT ( 20 ) NOT NULL COMMENT '用户id',
`username` VARCHAR ( 64 ) NOT NULL,
`password` VARCHAR ( 64 ) NOT NULL,
`fullname` VARCHAR ( 255 ) NOT NULL COMMENT '用户姓名',
`mobile` VARCHAR ( 11 ) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY ( `id` ) USING BTREE
) ENGINE = INNODB DEFAULT CHARSET = utf8 ROW_FORMAT = DYNAMIC
4.3.2.2 代码实现
1)定义 dataSource
在 application.properties
配置文件:
# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=rootroot
2)添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
3)定义 Dao
定义模型类型,在 model
包定义 UserDto
:
@Data
public class UserDto {
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
在 Dao
包定义 UserDao
:
package com.king.security.securityspringboot.dao;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
public UserDto getUserByUsername(String username){
String sql ="select id,username,password,fullname from t_user where username = ?";
List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
if(list == null || list.size() <= 0){
return null;
}
return list.get(0);
}
}
4.3.2.3 定义UserDetailService
在 service
包下定义 SpringDataUserDetailsService
类,实现 UserDetailsService
接口进行用户验证 :
package com.king.security.securityspringboot.service;
import com.king.security.securityspringboot.dao.UserDao;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* 自定UserDetails用户认证器
*/
// 注释不用,让它不加入Spring容器中
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
/**
* 测试此方法要把com.king.security.securityspringboot.config.userDetailsService()注释掉
* 根据用户账号查询信息
* @param username 用户账号
* @return 返回用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//登录账号
System.out.println("username="+username);
//根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
// UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
// return userDetails;
//根据账号去数据库查询...
UserDto userDto = userDao.getUserByUsername(username);
if(userDto == null){
return null;
}
// 这里的权限暂时使用静态数据,用户信息是动态数据
UserDetails userDetails = User.withUsername(userDto.getFullname()).password(userDto.getPassword()).authorities("p1").build();
return userDetails;
}
}
4.3.2.4 测试
输入账号和密码请求认证,跟踪代码。
4.3.2.5 使用BCryptPasswordEncoder
按照前边讲的 PasswordEncoder
的使用方法,使用 BCryptPasswordEncoder
需要完成如下工作:
1、在 WebSecurityConfig
安全配置类中定义 BCryptPasswordEncoder
:
/**
* 检验密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
// return NoOpPasswordEncoder.getInstance();
// 使用BCrypt方式进行匹配密码
return new BCryptPasswordEncoder();
}
2、UserDetails
中的密码存储 BCrypt
格式
前边实现了从数据库查询用户信息,所以数据库中的密码应该存储 BCrypt
格式:
4.4 会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security
提供会话管理,认证通过后将身份信息放入 SecurityContextHolder
上下文,SecurityContext
与当前线程进行绑定,方便获取 用户身份。
4.4.1.获取用户身份
编写 LoginController
类,实现 /r/r1
、/r/r2
的测试资源,并修改 login-success
方法,注意 getUsername
方法,Spring Security
获取当前登录用户信息的方法为 SecurityContextHolder.getContext().getAuthentication()
:
package com.king.security.securityspringboot.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录控制层
**/
@RestController
public class LoginController {
/**
* 认证成功将跳转到/login-success
* @return
*/
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
String username = getUsername();
return username + ": 登录成功";
}
/**
* 获取当前登录用户名
*/
private String getUsername(){
// Spring Security获取当前登录用户认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断没有认证就返回null
if(!authentication.isAuthenticated()){
return null;
}
// 获取用户身份
Object principal = authentication.getPrincipal();
String username = null;
// 判断 principal 是否 是UserDetails对象
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
// 将principal对象强转UserDetails对象拿用户名
username = ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
/**
* 测试资源1
* @return
*/
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
String username = getUsername();
return username + " 访问资源1";
}
/**
* 测试资源2
* @return
*/
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
String username = getUsername();
return username + " 访问资源2";
}
}
测试
登录前访问 http://localhost:8080/security-springboot/r/r1
资源。
被重定向至登录页面:
登录后访问资源。
成功访问资源,如下:
4.4.2会话控制
可以通过以下选项准确控制会话何时创建以及 Spring Security
如何与之交互:
机制 | 描述 |
---|---|
always | 如果没有session存在就创建一个 |
ifRequired | 如果需要就创建一个Session(默认)登录时 |
never | SpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它。 |
stateless | SpringSecurity将绝对不会创建Session,也不使用Session |
通过 WebSecurityConfig
文件以下配置方式对该选项进行配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}
默认情况下,Spring Security
会为每个登录成功的用户会新建一个session
,就是 ifRequired
。
若选用 never
,则指示 Spring Security
对登录成功的用户不创建 session
了,但若你的应用程序在某地方新建了 session
,那么 Spring Security
会用它的。
若使用 stateless
,则说明 Spring Security
对登录成功的用户不会创建 session
了,你的应用程序也不会允许新建 session
。并且它会暗示不使用 cookie
,所以每个请求都需要重新进行身份验证。这种无状态架构适用于 REST API
及其无状态认证机制。
4.4.2.1会话超时
可以再 sevlet
容器中设置 session
的超时时间,如下设置 session
有效期为 3600s
;
spring boot
配置文件:
server.servlet.session.timeout=3600s
session
超时之后,可以通过 Spring Security
设置跳转的路径。
http.sessionManagement()
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");
expired
指 session
过期,invalidSession
指传入的 sessionid
无效。
4.4.2.2 安全会话cookie
我们可以使用 httpOnly
和 secure
标签来保护我们的会话 cookie
:
httpOnly
:如果为true
,那么浏览器脚本将无法访问cookie
secure
:如果为true
,则cookie
将仅通过HTTPS
连接发送
spring boot 配置文件:
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
4.6 退出
Spring security
默认实现了 logout
退出,访问 /logout
,果然不出所料,退出功能 Spring
也替我们做好了。
点击 “Log Out”
退出成功。
退出 后访问其它 url
判断是否成功退出。
这里也可以自定义退出成功的页面:
在 WebSecurityConfig
文件的 protected void configure(HttpSecurity http)
方法中配置:
.and()
.logout() // 自定义退出
.logoutUrl("/logout") // 退出URL
.logoutSuccessUrl("/loginView?logout"); // 退出成功后的页面
当退出操作出发时,将发生:
- 使
HTTP Session
无效 - 清除
SecurityContextHolder
跳转到/loginView?logout
但是,类似于配置登录功能,咱们可以进一步自定义退出功能:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //开启登录配置
//...
.and()
.logout() (1)
.logoutUrl("/logout") (2)
.logoutSuccessUrl("/login‐view?logout") (3)
.logoutSuccessHandler(logoutSuccessHandler) (4)
.addLogoutHandler(logoutHandler) (5)
.invalidateHttpSession(true); (6)
}
(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter
会自动被应用;
(2)设置触发退出操作的URL (默认是 /logout )
;
(3)退出之后跳转的 URL
。默认是 /login?logout
;
(4)定制的 LogoutSuccessHandler
,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl()
的设置会被忽略;
(5)添加一个 LogoutHandler
,用于实现用户退出时的清理工作,默认SecurityContextLogoutHandler
会被添加为最后一个 LogoutHandler
;
(6)指定是否在退出时让 HttpSession
无效。 默认设置为 true
。
注意:如果让 logout
在 GET
请求下生效,必须关闭防止 CSRF
攻击 csrf().disable()
。如果开启了 CSRF
,必须使用 post
方式请求 /logout
。
logoutHandler
:
一般来说, LogoutHandler
的实现类被用来执行必要的清理,因而它们不应该抛出异常。 下面是 Spring Security
提供的一些实现:
PersistentTokenBasedRememberMeServices
基于持久化token
的RememberMe
功能的相关清理TokenBasedRememberMeService
基于token
的RememberMe
功能的相关清理CookieClearingLogoutHandler
退出时Cookie
的相关清理CsrfLogoutHandler
负责在退出时移除csrfToken
SecurityContextLogoutHandler
退出时SecurityContext
的相关清理
链式 API
提供了调用相应的 LogoutHandler
实现的快捷方式,比如 deleteCookies()
。
4.7 授权
4.7.1 概述
授权的方式包括 web
授权和方法授权
,web
授权是通过 url
拦截进行授权,方法授权
是通过方法拦截进行授权
。它们都会调用accessDecisionManager
进行授权决策,若为 web
授权则拦截器为 FilterSecurityInterceptor
;若为方法授权则拦截器为 MethodSecurityInterceptor
。如果同时通过 web
授权和方法授权则先执行web
授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。
类关系如下:
4.7.2.准备环境
4.7.2.1 数据库环境
在 t_user
表的 user_db
数据库创建如下表:
t_role
角色表:
CREATE TABLE `t_role` (
`id` VARCHAR ( 32 ) NOT NULL,
`role_name` VARCHAR ( 255 ) DEFAULT NULL,
`description` VARCHAR ( 255 ) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`status` CHAR ( 1 ) NOT NULL,
PRIMARY KEY ( `id` ),
UNIQUE KEY `unique_role_name` ( `role_name` )
) ENGINE = INNODB DEFAULT CHARSET = utf8
insert into `t_role`(`id`,`role_name`,`description`,`create_time`,`update_time`,`status`) values ('1','管理员',NULL,NULL,NULL,'');
t_user_role
用户角色关系表:
CREATE TABLE `t_user_role` (
`user_id` VARCHAR ( 32 ) NOT NULL,
`role_id` VARCHAR ( 32 ) NOT NULL,
`create_time` datetime DEFAULT NULL,
`creator` VARCHAR ( 255 ) DEFAULT NULL,
PRIMARY KEY ( `user_id`, `role_id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8
insert into `t_user_role`(`user_id`,`role_id`,`create_time`,`creator`) values ('1','1',NULL,NULL);
t_permission
权限表:
CREATE TABLE `t_permission` (
`id` VARCHAR ( 32 ) NOT NULL,
`code` VARCHAR ( 32 ) NOT NULL COMMENT '权限标识符',
`description` VARCHAR ( 64 ) DEFAULT NULL COMMENT '描述',
`url` VARCHAR ( 128 ) DEFAULT NULL COMMENT '请求地址',
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8
insert into `t_permission`(`id`,`code`,`description`,`url`) values ('1','p1','测试资源 1','/r/r1'),('2','p3','测试资源2','/r/r2');
t_role_permission
角色权限关系表:
CREATE TABLE `t_role_permission` (
`role_id` VARCHAR ( 32 ) NOT NULL,
`permission_id` VARCHAR ( 32 ) NOT NULL,
PRIMARY KEY ( `role_id`, `permission_id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8
insert into `t_role_permission`(`role_id`,`permission_id`) values ('1','1'),('1','2');
4.7.2.2 修改UserDetailService
1、修改 dao
接口
定义模型类型,在 model
包定义 PermissionsDto
:
package com.king.security.securityspringboot.model;
import lombok.Data;
@Data
public class PermissionsDto {
private String id;
private String code;
private String description;
private String url;
}
在 Dao
包的 UserDao
文件新增 findPermissionsByUserId()
方法:
package com.king.security.securityspringboot.dao;
import com.king.security.securityspringboot.model.PermissionsDto;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
public UserDto getUserByUsername(String username){
String sql ="select id,username,password,fullname from t_user where username = ?";
List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
if(list == null || list.size() <= 0){
return null;
}
return list.get(0);
}
/**
* 根据用户id查询用户权限
* @param userId
* @return
*/
public List<String> findPermissionsByUserId(String userId) {
String sql = "SELECT * FROM t_permission WHERE id IN( " +
"SELECT t_role_permission.permission_id FROM t_role_permission WHERE t_role_permission.role_id IN ( " +
"SELECT t_user_role.role_id FROM t_user_role WHERE t_user_role.user_id = ? ) )";
List<PermissionsDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionsDto.class));
List<String> permissionsList = new ArrayList<>();
list.forEach(permissionsDto -> permissionsList.add(permissionsDto.getCode()));
return permissionsList;
}
}
2、修改 UserDetailService
实现从数据库读取权限:
package com.king.security.securityspringboot.service;
import com.king.security.securityspringboot.dao.UserDao;
import com.king.security.securityspringboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* 自定UserDetails用户认证器
*/
// 注释不用,让它不加入Spring容器中
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
/**
* 测试此方法要把com.king.security.securityspringboot.config.userDetailsService()注释掉
* 根据用户账号查询信息
* @param username 用户账号
* @return 返回用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//登录账号
System.out.println("username="+username);
//根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
// UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
// return userDetails;
//根据账号去数据库查询...
UserDto userDto = userDao.getUserByUsername(username);
if(userDto == null){
return null;
}
//查询用户权限
List<String> permissionsList = userDao.findPermissionsByUserId(userDto.getId());
// 定义一个数据
String[] permissionsArray = new String[permissionsList.size()];
// 将 permissionsList 转成 数组
permissionsList.toArray(permissionsArray);
//创建userDetails
UserDetails userDetails = User.withUsername(userDto.getFullname()).password(userDto.getPassword()).authorities(permissionsArray).build();
return userDetails;
}
}
测试:
4.7.3.web授权
在上面例子中完成了认证拦截,并对 /r/**
下的某些资源进行简单的授权保护,但是想进行灵活的授权控制该怎么做呢?通过给 http.authorizeRequests()
添加多个子节点来定制需求到程序中的 URL
,如下代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() (1)
.antMatchers("/r/r1").hasAuthority("p1") (2)
.antMatchers("/r/r2").hasAuthority("p2") (3)
.antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')") (4)
.antMatchers("/r/**").authenticated() (5)
.anyRequest().permitAll() (6)
.and()
.formLogin() // 允许表单登录
// ...
}
(1) http.authorizeRequests()
方法有多个子节点,每个 macher
按照他们的声明顺序执行。
(2)指定"/r/r1"
URL
,拥有 p1
权限能够访问
(3)指定"/r/r2"
URL
,拥有 p2
权限能够访问
(4)指定了 "/r/r3"
URL
,同时拥有 p1
和 p2
权限才能够访问
(5)指定了除了 r1
、r2
、r3
之外 "/r/**"
资源,同时通过身份认证就能够访问,这里使用 SpEL(Spring Expression Language)
表达式
(6)剩余的尚未匹配的资源,不做保护。
注意:
规则的顺序是重要的,更具体的规则应该先写
。现在以 /admin
开始的所有内容都需要具有 ADMIN角色
的身份验证用户,即使是 /admin/login路径 (因为/admin/login已经被 /admin /**规则匹配,因此第二个规则被忽略)
。
.antMatchers("/admin/**").hasRole(“ADMIN”)
.antMatchers("/admin/login").permitAll()
因此,登录页面的规则
应该在 /admin/ **
规则之前,例如:
.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/**").hasRole(“ADMIN”)
保护 URL
常用的方法有:
authenticated()
保护 URL
,需要用户登录。
permitAll()
指定 URL
无需保护,一般应用与静态资源文件。
hasRole(String role)
限制单个角色访问,角色将被增加 “ROLE_”
所以 ”ADMIN”
将和 “ROLE_ADMIN”
进行比较。
hasAuthority(String authority)
限制单个权限访问。
hasAnyRole(String... roles)
允许多个角色访问。
hasAnyAuthority(String... authorities)
允许多个权限访问。
access(String attribute)
该方法使用 SpEL表达式, 所以可以创建复杂的限制。
hasIpAddress(String ipaddressExpression)
限制IP地址或子网。
推荐基于资源授权,而不是角色授权!
4.7.4.方法授权(注解方式授权)
现在我们已经掌握了使用如何使用 http.authorizeRequests()
对 web资源
进行授权保护,从 Spring Security2.0版 本
开始,它支持服务层方法的安全性的支持。本节学习 @PreAuthorize
, @PostAuthorize
, @Secured
三类注解。可以在任何 @Configuration
实例上使用 @EnableGlobalMethodSecurity
注释来启用基于注解的安全性。
@EnableGlobalMethodSecurity
三方法详解:https://www.jianshu.com/p/77b4835b6e8e
以下内容将启用 Spring Security
的 @Secured 注解
:
使用前提,Spring Security
默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter
的类上加 @EnableGlobalMethodSecurity( securedEnabled = true)
开启注解:
/**
* spring security安全配置
**/
@Configuration
@EnableGlobalMethodSecurity( securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
......
}
通过注解形式给控制层的方法设置权限授权:
package com.king.security.securityspringboot.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录控制层
**/
@RestController
public class LoginController {
/**
* 认证成功将跳转到/login-success
* @return
*/
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
String username = getUsername();
return username + ": 登录成功";
}
/**
* 获取当前登录用户名
*/
private String getUsername(){
// Spring Security获取当前登录用户认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断没有认证就返回null
if(!authentication.isAuthenticated()){
return null;
}
// 获取用户身份
Object principal = authentication.getPrincipal();
String username = null;
// 判断 principal 是否 是UserDetails对象
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
// 将principal对象强转UserDetails对象拿用户名
username = ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
/**
* 测试资源1
* @return
*/
//@PreAuthorize("hasAuthority('p1')") // 拥有 p1 权限才可以访问
//@PreAuthorize("hasAnyAuthority('p1','p3')") // 拥有 p1,p3 权限都可以访问
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
String username = getUsername();
return username + " 访问资源1";
}
/**
* 测试资源2
* @return
*/
//@PreAuthorize("hasAuthority('p2')") // 拥有 p2 权限才可以访问
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
String username = getUsername();
return username + " 访问资源2";
}
}
然后向方法(在类或接口上)添加注解就会限制对该方法的访问。 Spring Security
的原生注释支持为该方法定义了 一组属性。 这些将被传递给 AccessDecisionManager
以供它作出实际的决定:
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
以上配置标明 readAccount()
、findAccounts()
方法可匿名访问,底层使用 WebExpressionVoter
投票器,可从 AffirmativeBased
第23行代码跟踪。
post()
方法需要有 TELLER
角色才能访问,底层使用 RoleVoter
投票器。
使用如下代码可启用 prePost
注解的支持:
/**
* spring security安全配置
**/
@Configuration
@EnableGlobalMethodSecurity( prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
......
}
相应 Java
代码如下:
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
public Account post(Account account, double amount);
}
以上配置标明 readAccount()
、findAccounts()
方法可匿名访问,post()
方法需要同时拥有p_transfer
和 p_read_account
权限才能访问,底层使用 WebExpressionVoter
投票器,可从 AffirmativeBased
第23行代码跟踪。
5.分布式系统认证方案
5.1什么是分布式系统
随着软件环境和需求的变化 ,软件的架构由单体结构演变为分布式架构,具有分布式架构的系统叫分布式系统,分布式系统的运行通常依赖网络,它将单体结构的系统分为若干服务,服务之间通过网络交互来完成用户的业务处理,当前流行的微服务架构就是分布式系统架构,如下图:
分布式系统具体如下基本特点:
1、分布性
:每个部分都可以独立部署,服务之间交互通过网络进行通信,比如:订单服务、商品服务。
2、伸缩性
:每个部分都可以集群方式部署,并可针对部分结点进行硬件及软件扩容,具有一定的伸缩能力。
3、共享性
:每个部分都可以作为共享资源对外提供服务,多个部分可能有操作共享资源的情况。
4、开放性
:每个部分根据需求都可以对外发布共享资源的访问接口,并可允许第三方系统访问。
5.2 分布式认证需求
分布式系统的每个服务都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统内部服务提供认证,对第三方系统也要提供认证。分布式认证的需求总结如下:
统一认证授权
提供独立的认证服务,统一处理认证授权。
无论是不同类型的用户,还是不同种类的客户端(web端,H5、APP),均采用一致的认证、权限、会话机制,实现统一认证授权。
要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别等认证方式,并可以非常灵活的切换。
应用接入认证
应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分API给接入第三方使用,一方应用(内部 系统服 务)和三方应用(第三方应用)均采用统一机制接入。
5.3 分布式认证方案
5.3.1 选型分析
1、基于 session
的认证方式
在分布式的环境下,基于 session
的认证会出现一个问题,每个应用服务都需要在 session
中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将 session
信息带过去,否则会重新认证。
这个时候,通常的做法有下面几种:
Session复制
:多台应用服务器之间同步 session
,使 session
保持一致,对外透明。
Session黏贴
:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
Session集中存储
:将 Session
存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取Session。
总体来讲,基于 session
认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session
机制方式基于 cookie
,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高 session
的复制、黏贴及存储的容错性。
2、基于 token
的认证方式
基于 token
的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把 token
存在任意地方,并且可以实现 web
和 app
统一认证机制。其缺点也很明显,token
由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token
的签名验签操作也会给 cpu
带来额外的处理负担。
5.3.2 技术方案
根据 选型的分析,决定采用基于 token
的认证方式,它的优点是:
1、适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
2、token
认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0
、JWT
等。
3、一般情况服务端无需存储会话信息,减轻了服务端的压力。
分布式系统认证技术方案见下图:
流程描述:
(1)用户通过接入方(应用)登录,接入方采取 OAuth2.0
方式在统一认证服务 (UAA)
中认证。
(2)认证服务 (UAA)
调用验证该用户的身份是否合法,并获取用户权限信息。
(3)认证服务 (UAA)
获取接入方权限信息,并验证接入方是否合法。
(4)若登录用户以及接入方都合法,认证服务生成 jwt
令牌返回给接入方,其中 jwt
中包含了用户权限及接入方权限。
(5)后续,接入方携带 jwt
令牌对 API
网关内的微服务资源进行访问。
(6) API
网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。
(7)如果接入方的权限没问题,API
网关将原请求 header
中附加解析后的明文 token
,并将请求转发至微服务。
(8)微服务收到请求,明文 token
中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事: 1
,用户授权拦截(看当前用户是否有权访问该资源)。2
,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时 获取当前用户信息)。
流程所涉及到 UAA
服务、API
网关这三个组件职责如下:
1)统一认证服务(UAA)
它承载了 OAuth2.0
接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。
2)API网关
作为系统的唯一入口,API
网关为接入方提供定制的 API
集合,它可能还具有其它职责,如身份验证、监控、负载均衡、缓存等。API
网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
6. OAuth2.0
6.1 OAuth2.0介绍
OAuth(开放授权)
是一个开放标准,允许用户授权第三方应用访问它们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享它们数据的所有内容。OAuth2.0
是 OAuth协议
的延续版本,但不向后兼容OAuth 1.0
即完全废止了 OAuth1.0
。很多大公司如 Google
,Yahoo
,Microsoft
等都提供了 OAuth认证服务
,这些都足以说明 OAuth标准
逐渐成为开放资源授权的标准。
OAuth
协议目前发展到 2.0版本
,1.0版本
过于复杂,2.0版本
已得到广泛应用。
OAuth
协议:https://tools.ietf.org/html/rfc6749
下边分析一个 Oauth2认证
的例子,通过例子去理解 OAuth2.0协议
的认证流程,本例子是黑马程序员网站使用微信认证的过程,这个过程的简要描述如下:
用户借助微信认证登录 知乎网站
,用户就不用单独在 知乎网站
注册用户,怎么样算认证成功了? 知乎网站
需要成功从微信获取用户的身份信息则认为用户认证成功,那如何从微信获取用户的身份信息?用户信息的拥有者是用户本人,微信需要经过用户的同意方可为 知乎网站
生成令牌,知乎网站
拿此令牌方可从微信获取用户的信息。
1、客户端请求第三方授权
用户进入知乎网站
的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
点击“微信”出现一个二维码,此时用户扫描二维码,开始给 知乎网站
授权。
2、资源拥有者同意给客户端授权
资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给授权 知乎网站
访问自己的微信数据,用户点击 “同意”
表示同意授权,微信认证服务器会颁发一个授权码,并重定向到 知乎
的网站。
3、客户端获取到授权码,请求认证服务器申请令牌
此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
4、认证服务器向客户端响应令牌
微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 此交互过程用户看不到,当客户端拿到令牌后,用户在 知乎网站
看到已经登录成功。
5、客户端请求资源服务器的资源
客户端携带令牌访问资源服务器的资源。 知乎网站
携带令牌请求访问微信服务器获取用户的基本信息。
6、资源服务器返回受保护资源
资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
以上认证授权详细的执行流程如下:
通过上边的例子,大概了解了 OAauth2.0
的认证过程,下边看 OAuth2.0
认证流程:
引自 OAauth2.0
协议 rfc6749
https://tools.ietf.org/html/rfc6749 。
Client
:客户端;Resource Owner
:资源拥有者;Authorization Server
:授权服务器;Resource Server
:资源服务器;
- (A)用户打开客户端以后,客户端要求用户给予授权。
- (B)用户同意给予客户端授权。
- (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
- (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
- (E)客户端使用令牌,向资源服务器申请获取资源。
- (F)资源服务器确认令牌无误,同意向客户端开放资源
上述的六个步骤,B
是关键,即用户如何给客户端进行授权。有了授权之,客户端就可以获取令牌,进而凭令牌获取资源。
OAauth2.0
包括以下角色:
1、客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android
客户端、Web
客户端(浏览器端)、微信客户端等。
2、资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。
3、授权服务器(也称认证服务器)
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token)
,作为客户端访问资源服务器的凭据。本例为微信的认证服务器。
4、资源服务器
存储资源的服务器,本例子为微信存储的用户信息。
现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?
答案是否定的
,服务提供商会给准入的接入方一个身份,用于接入时的凭据:
client_id
:客户端标识
client_secret
:客户端秘钥
因此,准确来说,授权服务器对
两种 OAuth2.0
中的两个角色进行认证授权
,分别是资源拥有者
、客户端
。
6.2 Spring Cloud Security OAuth2
6.2.1 环境介绍
Spring-Security-OAuth2
是对 OAuth2
的一种实现,并且跟之前学习的 Spring Security
相辅相成,与 Spring Cloud体系
的集成也非常便利,接下来,需要对它进行学习,最终使用它来实现案例设计的分布式认证授权解决方案。
OAuth2.0
的服务提供方涵盖两个服务,即 授权服务 (Authorization Server,也叫认证服务)
和 资源服务 (Resource Server)
,使用 Spring Security OAuth2
的时候可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。
授权服务 (Authorization Server)
应包含对接入端以及登入用户的合法性进行验证并颁发 token
等功能,对令牌的请求端点由 Spring MVC
控制器进行实现,下面是配置一个认证服务必须要实现的 endpoints
:
AuthorizationEndpoint
服务于认证请求。默认 URL:/oauth/authorize
。TokenEndpoint
服务于访问令牌的请求。默认 URL:/oauth/token
。
资源服务 (Resource Server)
应包含对资源的保护功能,对非法请求进行拦截,对请求中 token
进行解析鉴权等,下面的过滤器用于实现 OAuth 2.0
资源服务:
OAuth2AuthenticationProcessingFilter
用来对请求给出的身份令牌解析鉴权。
本教程分别创建 uaa授权服务(也可叫认证服务)
和 order订单资源服务
:
认证流程如下:
1、客户端请求 UAA
授权服务进行认证。
2、认证通过后由 UAA
颁发令牌。
3、客户端携带令牌 token
请求资源服务
4、资源服务校验令牌的合法性,合法即返回资源信息。
6.2.2 环境搭建
6.2.2.1 父工程
创建 distributed‐security
项目为 maven
工程作为父工程,依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>2.6.1</version>-->
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.king.security</groupId>
<artifactId>distributed-security</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 指定父级工程 -->
<packaging>pom</packaging>
<modules>
<!-- 引入子模块名称 -->
<module>distributed-security-uaa</module>
</modules>
<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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</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.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<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>untitled/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>untitled/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<!--<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</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>
</configuration>
</plugin>
</plugins>
</build>
</project>
6.2.2.2 创建UAA授权服务工程
1、创建 distributed-security-uaa
子工程
创建 distributed-security-uaa
作为授权服务工程,依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 引入父级模块名称 -->
<parent>
<artifactId>distributed-security</artifactId>
<groupId>com.king.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!-- 子模块名称 -->
<artifactId>distributed-security-uaa</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
工程结构如下:
2、启动类
本工程采用 SpringBoot
开发,每个工程编写一个启动类:
package com.king.security.distributed.uaa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"com.king.security.distributed.uaa"})
public class UAAServer {
public static void main(String[] args) {
SpringApplication.run(UAAServer.class, args);
}
}
3、配置文件
在 resources
下创建 application.properties
配置文件:
spring.application.name=uaa-service
server.port=53020
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 = /uaa
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
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_db?characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=rootroot
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
6.2.2.3 创建Order资源服务工程
本工程为Order订单服务工程,访问本工程的资源需要认证通过。
本工程的目的主要是测试认证授权的功能,所以不涉及订单管理相关业务。
1、创建 Order
工程,pom.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 引入父级模块名称 -->
<parent>
<artifactId>distributed-security</artifactId>
<groupId>com.king.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!-- 子模块名称 -->
<artifactId>distributed-security-order</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</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>
</project>
2、工程结构:
3、配置文件
在 resources
中创建 application.properties
配置文件:
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
6.2.2.授权服务器配置
6.2.2.1 EnableAuthorizationServer
可以用 @EnableAuthorizationServer
注解并继承 AuthorizationServerConfigurerAdapter
来配置 OAuth2.0
授权服务器。
在distributed-security-uaa
工程的 Config
包下创建 AuthorizationServer
配置文件 :
/**
* @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// 略...
}
AuthorizationServerConfigurerAdapter
要求配置以下几个类,这几个类是由 Spring
创建的独立的配置对象,它们会被 Spring
传入AuthorizationServerConfigurer
中进行配置:
package com.king.security.distributed.uaa.config;
import org.springframework.context.annotation.Configuration;
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;
/**
* 授权配置类
* @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
public AuthorizationServer() {
super();
}
/**
* 用来配置令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
}
/**
* 配置客户端详情服务,客户端鉴权配置 client_id :客户端标识、client_secret :客户端秘钥
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
}
/**
* 用来配置令牌(token)的访问端点和令牌服务(token services)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
}
}
ClientDetailsServiceConfigurer
:用来配置客户端详情服务(ClientDetailsService
),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。AuthorizationServerEndpointsConfigurer
:用来配置令牌(token
)的访问端点和令牌服务(token services
)。AuthorizationServerSecurityConfigurer
:用来配置令牌端点的安全约束。
大体流程:ClientDetailsServiceConfigurer
客户端来 AuthorizationServerEndpointsConfigurer
申请令牌,申请完令牌之后需要 AuthorizationServerSecurityConfigurer
管理约束令牌。
6.2.2.2.1 配置客户端详细信息
ClientDetailsServiceConfigurer
能够使用内存或者 JDBC
来实现客户端详情服务(ClientDetailsService
), ClientDetailsService
负责查找 ClientDetails
,而ClientDetails
有几个重要的属性如下列表:
clientId
:(必须的)用来标识客户的id
。secret
:(需要值得信任的客户端)客户端安全码,如果有的话。scope
:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。authorizedGrantTypes
:此客户端可以使用的授权类型,默认为空。authorities
:此客户端可以使用的权限(基于Spring Security authorities
)。
客户端详情(Client Details
)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService
)或者通过自己实现 ClientRegistrationService
接口(同时你也可以实现 ClientDetailsService
接口)来进行管理。
暂时使用内存方式存储客户端详情信息,在distributed-security-uaa
工程的 Config
包下的 AuthorizationServer
配置文件,public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
方法配置如下:
/**
* 配置客户端详情服务,客户端鉴权配置 client_id :客户端标识、client_secret :客户端秘钥
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 暂时使用内存方式存储客户端详情信息
clients.inMemory()// 使用in-memory存储
.withClient("c1")// client_id 客户端ID
.secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
.resourceIds("res1")// 资源列表
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)// false会跳转到授权页面
.redirectUris("http://www.baidu.com");// 加上验证回调地址
// .and() // 配置多个客户端用 and 继续配置
// .withClient("c2")// client_id 客户端ID
// .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
// .resourceIds("res1")// 资源列表
// .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
// .scopes("all")// 允许的授权范围
// .autoApprove(false)// false会跳转到授权页面
// .redirectUris("http://www.zhihu.com");// 加上验证回调地址
}
6.2.2.2.2 配置自定义UserDetailsService
在distributed-security-uaa
工程的 model
包下定义 UserDto 用户信息
、PermissionsDto 权限信息
类:
package com.king.security.distributed.uaa.model;
import lombok.Data;
@Data
public class UserDto {
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
package com.king.security.distributed.uaa.model;
import lombok.Data;
@Data
public class PermissionsDto {
private String id;
private String code;
private String description;
private String url;
}
在distributed-security-uaa
工程的 dao
包下定义 UserDao
类:
package com.king.security.distributed.uaa.dao;
import com.king.security.distributed.uaa.model.PermissionsDto;
import com.king.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
public UserDto getUserByUsername(String username){
String sql ="select id,username,password,fullname from t_user where username = ?";
List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
if(list == null || list.size() <= 0){
return null;
}
return list.get(0);
}
/**
* 根据用户id查询用户权限
* @param userId
* @return
*/
public List<String> findPermissionsByUserId(String userId) {
String sql = "SELECT * FROM t_permission WHERE id IN( " +
"SELECT t_role_permission.permission_id FROM t_role_permission WHERE t_role_permission.role_id IN ( " +
"SELECT t_user_role.role_id FROM t_user_role WHERE t_user_role.user_id = ? ) )";
List<PermissionsDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionsDto.class));
List<String> permissionsList = new ArrayList<>();
list.forEach(permissionsDto -> permissionsList.add(permissionsDto.getCode()));
return permissionsList;
}
}
在distributed-security-uaa
工程的 service
包下自定义配置 UserDetailsService
类:
package com.king.security.distributed.uaa.service;
import com.king.security.distributed.uaa.dao.UserDao;
import com.king.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 自定UserDetailsService用户认证器
*/
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
/**
* 根据用户账号查询信息
* @param username 用户账号
* @return 返回用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//登录账号
System.out.println("username="+username);
//根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
// UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
// return userDetails;
//根据账号去数据库查询...
UserDto userDto = userDao.getUserByUsername(username);
if(userDto == null){
return null;
}
//查询用户权限
List<String> permissionsList = userDao.findPermissionsByUserId(userDto.getId());
// 定义一个数据
String[] permissionsArray = new String[permissionsList.size()];
// 将 permissionsList 转成 数组
permissionsList.toArray(permissionsArray);
// 将userDto转成json
String principal = JSON.toJSONString(userDto);
// 创建userDetails
UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionsArray).build();
return userDetails;
}
}
6.2.2.2.3 管理令牌
AuthorizationServerTokenServices
接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。
自己可以创建 AuthorizationServerTokenServices
这个接口的实现,则需要继承 DefaultTokenServices
这个类, 里面包含了一些有用实现,可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore
接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore
这个接口有一个默认的实现,它就是 InMemoryTokenStore
,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了 TokenStore
接口:
InMemoryTokenStore
:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。JdbcTokenStore
:这是一个基于JDBC
的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"
这个依赖加入到你的classpath
当中。JwtTokenStore
:这个版本的全称是JSON Web Token(JWT)
,它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token
)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore
不会保存任何数据,但是它在转换令牌值以及授权信息方面与DefaultTokenServices
所扮演的角色是一样的。
1、定义 TokenConfig
在distributed-security-uaa
工程的 config
包下定义 TokenConfig
,暂时先使用 InMemoryTokenStore
( token
存储在内存) token
生成策略时,系统默认的token
的有效时间是 12
小时 ,生成一个普通的令牌:
package com.king.security.distributed.uaa.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.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* token令牌配置类
*/
@Configuration
public class TokenConfig {
/**
* 令牌储存策略
* @return
*/
@Bean
public TokenStore tokenStore() {
// 使用内存存储令牌(普通令牌)
return new InMemoryTokenStore();
}
}
2、定 AuthorizationServerTokenServices
在distributed-security-uaa
工程的 Config
包下的 AuthorizationServer
配置文件中定义 AuthorizationServerTokenServices()
方法:
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌管理服务
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客户端详情服务
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
6.2.2.2.4 令牌访问端点配置
AuthorizationServerEndpointsConfigurer
这个对象的实例可以完成令牌服务以及令牌 endpoint
配置。
配置授权类型(Grant Types)
:
AuthorizationServerEndpointsConfigurer
通过设定以下属性决定支持的授权类型(Grant Types)
:
authenticationManager
:认证管理器,当你选择了资源所有者密码(password
)授权类型的时候,请设置这个属性注入一个AuthenticationManager
对象。userDetailsService
:如果你设置了这个属性的话,那说明你有一个自己的UserDetailsService
接口的实现, 或者你可以把这个东西设置到全局域上面去(例如GlobalAuthenticationManagerConfigurer
这个配置对象),当你设置了这个之后,那么"refresh_token"
即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。authorizationCodeServices
:这个属性是用来设置授权码服务的(即AuthorizationCodeServices
的实例对 象),主要用于"authorization_code"
授权码类型模式。implicitGrantService
:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。tokenGranter
:当你设置了这个东西(即TokenGranter
接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。
配置授权端点的URL(Endpoint URLs)
:
AuthorizationServerEndpointsConfigurer
这个配置对象有一个叫做 pathMapping()
的方法用来配置端点 URL
链接,它有两个参数:
- 第一个参数:
String
类型的,这个端点URL
的默认链接。 - 第二个参数:
String
类型的,你要进行替代的URL
链接。
以上的参数都将以 "/"
字符为开始的字符串,框架的默认 URL
链接如下列表,可以作为这个 pathMapping()
方法的第一个参数:
/oauth/authorize
:授权端点。/oauth/token
:令牌端点。/oauth/confirm_access
:用户确认授权提交端点。/oauth/error
:授权服务错误信息端点。/oauth/check_token
:用于资源服务访问的令牌解析端点。/oauth/token_key
:提供公有密匙的端点,如果你使用JWT
令牌的话。
需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问。
在distributed-security-uaa
工程的 Config
包下的 AuthorizationServer
配置文件中配置令牌访问端点:
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
/**
* 设置授权码模式的授权码如何存取
* @param dataSource
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 用来配置令牌(token)的访问端点和令牌服务(token services)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理器(密码模式需要)
.authorizationCodeServices(authorizationCodeServices)//授权码服务需要
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允信post提交
}
6.2.2.2.5 令牌端点的安全约束
AuthorizationServerSecurityConfigurer
:用来配置令牌端点(Token Endpoint
)的安全约束,在distributed-security-uaa
工程的 Config
包下的 AuthorizationServer
配置文件中配置如下:
/**
* 用来配置令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") (1)
.checkTokenAccess("permitAll()") (2)
.allowFormAuthenticationForClients(); (3)
}
(1) tokenkey
这个 endpoint
当使用 JwtToken
且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint
完全公开。
(2)checkToken
校验令牌这个 endpoint
端点完全公开。
(3) 允许表单认证。
授权服务配置总结
:
授权服务配置分成三大块,可以关联记忆。
- 既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。
- 既然要颁发
token
,那必须得定义token
的相关endpoint
端点 ,以及token
如何存取,以及客户端支持哪些类型的token
。 - 既然暴露除了一些
endpoint
端点,那对这些endpoint
端点可以定义一些安全上的约束等。
6.2.2.2.6 web安全配置
在distributed-security-uaa
工程的 Config
包下定义 WebSecurityConfig
配置文件:
package com.king.security.distributed.uaa.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* spring security安全配置
* @EnableGlobalMethodSecurity:Spring Security默认是禁用注解的,要想开启注解,
* 需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,
* 来判断用户对某个控制层的方法是否具有访问权限,
* securedEnabled: 确定安全注解 [@Secured] 是否启用
* prePostEnabled: 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用
* jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed..]是否启用
**/
@Configuration
@EnableGlobalMethodSecurity( securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户信息认证服务(认证管理器)
* @return
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 检验密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 无需编码器进行匹配密码
// return NoOpPasswordEncoder.getInstance();
// 使用BCrypt方式进行匹配密码
return new BCryptPasswordEncoder();
}
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // csrf().disable():屏蔽CSRF控制,即spring security不再限制CSRF
.authorizeRequests() //开启登录配置
.antMatchers("/r/r1").hasAuthority("p1") // 有p1权限才能访问
.antMatchers("/login*")
.permitAll() //除了/login*",其它的请求可以访问
.anyRequest().authenticated()
.and()
.formLogin(); //允许表单登录
}
}
6.2.2.2.7 oauth_code 跟 oauth_client_details 数据库表
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `oauth_code` VALUES ('Ykj769', '2020-12-13 16:18:25', 0xACED0005737200416F72672E737072696E676672616D65776F726B2E73656375726974792E6F61757468322E70726F76696465722E4F417574683241757468656E7469636174696F6EBD400B02166252130200024C000D73746F7265645265717565737474003C4C6F72672F737072696E676672616D65776F726B2F73656375726974792F6F61757468322F70726F76696465722F4F4175746832526571756573743B4C00127573657241757468656E7469636174696F6E7400324C6F72672F737072696E676672616D65776F726B2F73656375726974792F636F72652F41757468656E7469636174696F6E3B787200476F72672E737072696E676672616D65776F726B2E73656375726974792E61757468656E7469636174696F6E2E416273747261637441757468656E7469636174696F6E546F6B656ED3AA287E6E47640E0200035A000D61757468656E746963617465644C000B617574686F7269746965737400164C6A6176612F7574696C2F436F6C6C656374696F6E3B4C000764657461696C737400124C6A6176612F6C616E672F4F626A6563743B787000737200266A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654C697374FC0F2531B5EC8E100200014C00046C6973747400104C6A6176612F7574696C2F4C6973743B7872002C6A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65436F6C6C656374696F6E19420080CB5EF71E0200014C00016371007E00047870737200136A6176612E7574696C2E41727261794C6973747881D21D99C7619D03000149000473697A65787000000002770400000002737200426F72672E737072696E676672616D65776F726B2E73656375726974792E636F72652E617574686F726974792E53696D706C654772616E746564417574686F7269747900000000000001FE0200014C0004726F6C657400124C6A6176612F6C616E672F537472696E673B787074000270317371007E000D74000270337871007E000C707372003A6F72672E737072696E676672616D65776F726B2E73656375726974792E6F61757468322E70726F76696465722E4F41757468325265717565737400000000000000010200075A0008617070726F7665644C000B617574686F72697469657371007E00044C000A657874656E73696F6E7374000F4C6A6176612F7574696C2F4D61703B4C000B726564697265637455726971007E000E4C00077265667265736874003B4C6F72672F737072696E676672616D65776F726B2F73656375726974792F6F61757468322F70726F76696465722F546F6B656E526571756573743B4C000B7265736F7572636549647374000F4C6A6176612F7574696C2F5365743B4C000D726573706F6E7365547970657371007E0016787200386F72672E737072696E676672616D65776F726B2E73656375726974792E6F61757468322E70726F76696465722E426173655265717565737436287A3EA37169BD0200034C0008636C69656E74496471007E000E4C001172657175657374506172616D657465727371007E00144C000573636F706571007E001678707400026331737200256A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654D6170F1A5A8FE74F507420200014C00016D71007E00147870737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F400000000000067708000000080000000474000D726573706F6E73655F74797065740004636F6465740009636C69656E745F696471007E001974000C72656469726563745F75726C74001568747470733A2F2F7777772E62616964752E636F6D74000573636F706574000A524F4C455F41444D494E78737200256A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65536574801D92D18F9B80550200007871007E0009737200176A6176612E7574696C2E4C696E6B656448617368536574D86CD75A95DD2A1E020000787200116A6176612E7574696C2E48617368536574BA44859596B8B7340300007870770C000000103F4000000000000171007E002478017371007E0028770C000000103F40000000000000787371007E001C3F40000000000000770800000010000000007874001568747470733A2F2F7777772E62616964752E636F6D707371007E0028770C000000103F4000000000000174000472657331787371007E0028770C000000103F4000000000000171007E001F787372004F6F72672E737072696E676672616D65776F726B2E73656375726974792E61757468656E7469636174696F6E2E557365726E616D6550617373776F726441757468656E7469636174696F6E546F6B656E00000000000001FE0200024C000B63726564656E7469616C7371007E00054C00097072696E636970616C71007E00057871007E0003017371007E00077371007E000B0000000277040000000271007E000F71007E00117871007E0033737200486F72672E737072696E676672616D65776F726B2E73656375726974792E7765622E61757468656E7469636174696F6E2E57656241757468656E7469636174696F6E44657461696C7300000000000001FE0200024C000D72656D6F74654164647265737371007E000E4C000973657373696F6E496471007E000E787074000F303A303A303A303A303A303A303A31740020413742323638324643314142443130313035324444414341353444413343453170737200326F72672E737072696E676672616D65776F726B2E73656375726974792E636F72652E7573657264657461696C732E5573657200000000000001FE0200075A00116163636F756E744E6F6E457870697265645A00106163636F756E744E6F6E4C6F636B65645A001563726564656E7469616C734E6F6E457870697265645A0007656E61626C65644C000B617574686F72697469657371007E00164C000870617373776F726471007E000E4C0008757365726E616D6571007E000E7870010101017371007E0025737200116A6176612E7574696C2E54726565536574DD98509395ED875B0300007870737200466F72672E737072696E676672616D65776F726B2E73656375726974792E636F72652E7573657264657461696C732E5573657224417574686F72697479436F6D70617261746F7200000000000001FE020000787077040000000271007E000F71007E00117870740008746F6D7368696469);
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(64) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` longtext,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`archived` tinyint(4) DEFAULT NULL,
`trusted` tinyint(4) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `oauth_client_details` VALUES ('c1', 'res1', '$2a$10$Dappn49nM8ZjkCO3ASwR/evnHQN3iPH4npP45js0d9Ug3Vm4ui.sq', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2', 'secret', 'ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');
6.2.3.授权码模式
6.2.3.1 授权码模式介绍
下图是授权码模式交互图:
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
参数列表如下:
client_id
:客户端准入标识。response_type
:授权码模式固定为code
。scope
:客户端权限。redirect_uri
:跳转uri
,当授权码申请成功后会跳转到此地址,并在后边带上code
参数(授权码)。
(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权服务器将授权码(AuthorizationCode
)转经浏览器发送给client(通过redirect_uri)
。
(4)客户端拿着授权码向授权服务器索要访问 access_token
,请求如下:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
参数列表如下:
client_id
:客户端准入标识。client_secret
:客户端秘钥。grant_type
:授权类型,填写authorization_code
,表示授权码模式。code
:授权码就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。redirect_uri
:申请授权码时的跳转url
,一定和申请授权码时用的redirect_uri
一致。
(5)授权服务器返回令牌(access_token
):
这种模式是四种模式中最安全的一种模式。
一般用于 client
是 Web
服务器端应用或第三方的原生 App
调用资源服务的时候。因为在这种模式中 access_token
不会经过浏览器或移动端的 App
,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。
6.2.3.2 测试授权码模式
启动distributed-security-uaa
工程,在浏览器访问认证页面:
GET http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
访问时被重定向到登录页,因为没有登录认证:
进行认证登录即可,认证之后跳转到授权页面:
确认授权后,浏览器会重定向到指定路径(oauth_client_details表
中的 web_server_redirect_uri
)并附加验证码? code=igAgar
(每次不一样),最后使用该验证码获取 token
。
获取
token
令牌URL
:http://localhost:53020/uaa/oauth/token
6.2.4.简化模式
6.2.4.1 简化模式介绍下
图是简化模式交互图:
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:
http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
参数描述同**授权码模式
** ,注意 response_type=token
,说明是简化模式。
(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权服务器将授权码将令牌(access_token
)以 Hash
的形式存放在重定向uri的fargment中发送给浏览器。
注: fragment
主要是用来标识 URI
所标识资源里的某个资源,在 URI
的末尾通过 (#
)作为 fragment
的开头, 其中 #
不属于 fragment
的值。如 https://domain/index#L18
这个 URI
中 L18
就是 fragment
的值。只需要知道 js
通过响应浏览器地址栏变化的方式能获取到 fragment
就行了。
一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。
6.2.4.2 测试简化模式
启动distributed-security-uaa
工程,在浏览器访问认证页面:
http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
访问时被重定向到登录页,因为没有登录认证:
进行认证登录即可,认证之后跳转到授权页面:
确认授权后,浏览器会重定向到指定路径(oauth_client_details表
中的web_server_redirect_uri
)并以 Hash
的形式存放在重定向 uri
的 fargment
中,如:
https://www.baidu.com/#access_token=ef2d6701-7a74-4565-b8f5-2889b5283ac9&token_type=bearer&expires_in=3952
6.2.5.密码模式
6.2.5.1 授权码模式介绍
下图是密码模式交互图:
(1)资源拥有者将用户名、密码发送给客户端
(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token
),请求如下:
http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
参数列表如下:
client_id
:客户端准入标识。client_secre
:客户端秘钥。grant_type
:授权类型,填写password
表示密码模式。username:资源拥有者用户名
,password:资源拥有者密码
。
(3)授权服务器将令牌(access_token
)发送给client
:
这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了 client
,因此这就说明这种模式只能用于 client
是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生 App
或第一方单页面应用。
6.2.5.2 测试密码模式
POST http://localhost:53020/uaa/oauth/token
参数列表如下:
6.2.6.客户端模式
6.2.6.1 客户端模式介绍
(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token
)。
(2)确认客户端身份无误后,将令牌(access_token
)发送给 client
,请求如下:
http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
参数列表如下:
client_id
:客户端准入标识。client_secre
:客户端秘钥。grant_type
:授权类型,填写client_credentials
表示客户端模式。
这种模式是最方便但最不安全的模式
。因此这就要求我们对 client
完全的信任,而 client
本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。
6.2.6.2 测试客户端模式
POST http://localhost:53020/uaa/oauth/toke
请求参数:
6.2.7.资源服务测试
6.2.7.1 资源服务器配置
@EnableResourceServer
注解到一个 @Configuration
配置类上,并且必须使用 ResourceServerConfigurer
这个配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter
然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性:
ResourceServerSecurityConfigurer
中主要包括:
tokenServices
:ResourceServerTokenServices
类的实例,用来实现令牌服务。tokenStore
:TokenStore
类的实例,指定令牌如何访问,与tokenServices
配置可选 。resourceId
: 这个资源服务的ID
,这个属性是可选的,但是推荐设置并在授权服务中进行验证。 其他的拓展属性例如tokenExtractor
令牌提取器用来提取请求中的令牌。
HttpSecurity
配置这个与 Spring Security
类似:
- 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
- 通过
http.authorizeRequests()
来设置受保护资源的访问规则。 - 其他的自定义权限保护规则通过
HttpSecurity
来进行配置。
@EnableResourceServer
注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter
的过滤器链。
在distributed-security-order
工程的 Config
包下定义编写 ResouceServerConfig
配置文件:
package com.king.security.distributed.order.config;
import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* 资源服务配置类
* @EnableResourceServer:资源服务,提供基于token的资源访问功能。
*/
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 资源ID,这个要跟授权服务端的授权资源列表的值对应才能正确找到资源
*/
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
/**
* 资源服务端令牌解析服务
* @return
*/
@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;
}
/**
* 资源服务器安全配置器
* @param resources
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
// .tokenStore(tokenStore)
.tokenServices(tokenService())//验证令牌的服务
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')") //授权范围
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 现在基于token形式,这里就不记录session了
}
}
6.2.7.2 验证token
ResourceServerTokenServices
是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices
,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices
,它知道如何对令牌进行解码。
令牌解析方法: 使用 DefaultTokenServices
在资源服务器本地配置令牌存储、解码、解析方式 使用 RemoteTokenServices
资源服务器通过 HTTP
请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token
使用授权服务的 /oauth/check_token
端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问, 这在咱们授权服务配置中已经提到了,下面是一个例子,在这个例子中,我们在distributed-security-uaa
工程授权服务端的 Config
包下 AuthorizationServer
授权类中配置了 /oauth/check_token
和 /oauth/token_key
这两个端点:
package com.king.security.distributed.uaa.config;
import org.springframework.beans.factory.annotation.Autowired;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import javax.sql.DataSource;
/**
* 授权配置类
* @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
//略......
/**
* 用来配置令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")// http://localhost:53010/uaa/oauth/token 获取token令牌这个endpoint端点当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个permitAll()完全公开
.checkTokenAccess("permitAll()")// http://localhost:53020/uaa/oauth/check_token 校验token令牌这个endpoint端点完全公开
.allowFormAuthenticationForClients();// 允许表单认证
}
}
在distributed-security-order
工程的 Config
包下的 ResouceServerConfig
中配置 RemoteTokenServices
资源服务令牌解析服务:
/**
* 资源ID,这个要跟授权服务端的授权资源列表的值对应才能正确找到资源
*/
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
/**
* 资源服务端令牌解析服务
* @return
*/
@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;
}
/**
* 资源服务器安全配置器
* @param resources
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
// .tokenStore(tokenStore)
.tokenServices(tokenService())//验证令牌的服务
.stateless(true);//用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
}
测试 http://localhost:53020/uaa/oauth/check_token
验证 token
令牌 URL
:
6.2.7.3 编写控制层
在distributed-security-order
工程的 controller
包下编写 OrderController
,此 controller
表示订单资源的访问类:
package com.king.security.distributed.order.controller;
import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
public String r1(){
return "访问资源1";
}
}
6.2.7.4 添加安全访问控制
package com.king.security.distributed.order.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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* spring security安全配置
* @EnableGlobalMethodSecurity:Spring Security默认是禁用注解的,要想开启注解,
* 需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,
* 来判断用户对某个控制层的方法是否具有访问权限,
* securedEnabled: 确定安全注解 [@Secured] 是否启用
* prePostEnabled: 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用
* jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed..]是否启用
**/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/r/r1").hasAuthority("p2")
// .antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll();//除了/r/**,其它的请求可以访问
}
}
6.2.7.5 测试
1、同时启动 distributed-security-uaa
授权服务端跟 distributed-security-order
资源服务端,在没有授权 token
令牌时或授权 token
令牌是错误的,直接访问 distributed-security-order
工程的资源服务端是会被拒绝:
2、申请令牌
这里我们使用密码方式:
3、请求资源
按照 Oauth2.0协议
要求,请求资源需要携带token,如下: token的参数名称为:Authorization,值为:Bearer token值
6.3 JWT令牌
6.3.1 JWT介绍
通过上边的测试发现,当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices 远程请求授权服务验证 token
,如果访问量较大将会影响系统的性能
。
解决上边问题:
令牌采用 JWT格式
即可解决上边的问题,用户认证通过会得到一个 JWT令牌
,JWT令牌
中已经包括了用户相关的信息,客户端只需要携带 JWT
访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权
。
1、什么是JWT?
JSON Web Token(JWT)
是一个开放的行业标准(RFC 7519
),它定义了一种简介的、自包含的协议格式,用于在通信双方传递 json对象
,传递的信息经过数字签名可以被验证和信任。JWT
可以使用 HMAC算法
或使用 RSA的公钥/私钥对来签名
,防止被篡改。
官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
jwt
基于json
,非常方便解析。- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法及数字签名技术,
JWT
防止篡改,安全性高。 - 资源服务端使用
JWT
可不依赖认证服务端即可完成授权。
缺点:
JWT
令牌较长,占存储空间比较大。
2、JWT令牌结构
通过学习 JWT
令牌结构为自定义 jwt
令牌打好基础。
JWT
令牌由三部分组成,每部分中间使用点(.
)分隔,比如:xxxxx.yyyyy.zzzzz
Header
: 头部包括令牌的类型(即JWT
)及使用的哈希算法(如HMAC SHA256
或RSA
)。
一个例子如下:
下边是Header
部分的内容
{
“alg”: “HS256”,
“typ”: “JWT”
}
Payload
: 第二部分是内容负载,内容也是一个json
对象,它是存放有效信息的地方,它可以存放jwt
提供的现成字段,比如 :iss(签发者)
,exp(过期时间戳)
,sub(面向的用户)
等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用Base64Url
编码,得到一个字符串就是JWT
令牌的第二部分。 一个例子:
{
“sub”: “1234567890”,
“name”: “456”,
“admin”: true
}
Signature
: 第三部分是签名,此部分用于防止jwt
内容被篡改。
这个部分使用base64url
将前两部分进行编码,编码后使用点(.
)连接组成字符串,最后使用header
中声明签名算法进行签名。
一个例子:
签名算法 HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
base64UrlEncode(header)
: jwt
令牌的第一部分。
base64UrlEncode(payload)
: jwt
令牌的第二部分。
secret
: 签名所使用的密钥。
6.3.2 配置JWT令牌服务
在distributed-security-uaa
授权工程中配置 jwt
令牌服务,即可实现生成 jwt
格式的令牌。
1、TokenConfig
package com.king.security.distributed.uaa.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.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* token令牌配置类
*/
@Configuration
public class TokenConfig {
/**
* 签名密钥
*/
private String SIGNING_KEY = "uaa123";
/**
* 令牌储存策略
* @return
*/
// @Bean
// public TokenStore tokenStore() {
// // 使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Bean
public TokenStore tokenStore() {
//JWT令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
/**
* JWT访问令牌转换器(JWT算法)
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}
2、定义 JWT
令牌服务
在distributed-security-uaa
授权工程中修改 AuthorizationServer
授权配置类的 tokenService()
方法:
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
/**
* 令牌管理服务
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客户端详情服务
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
// 令牌增强,生成JWT令牌
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
6.3.3 生成jwt令牌
同时启动 distributed-security-uaa
授权服务端跟 distributed-security-order
资源服务端,以 POST
方式访问 http://localhost:53020/uaa/oauth/token
:
6.3.4 校验jwt令牌
资源服务需要和授权服务拥有一致的签字、令牌服务等:
1、将授权服务端中的 TokenConfig类
拷贝到资源服务端中
2、屏蔽资源服务端原来的令牌服务类方法
package com.king.security.distributed.order.config;
import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* 资源服务配置类
* @EnableResourceServer:资源服务,提供基于token的资源访问功能。
*/
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 资源ID,这个要跟授权服务端的授权资源列表的值对应才能正确找到资源
*/
public static final String RESOURCE_ID = "res1";
/**
* 注入令牌储存策略
*/
@Autowired
TokenStore tokenStore;
/**
* 资源服务端令牌解析服务
* @return
*/
// @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;
// }
/**
* 资源服务器安全配置器
* @param resources
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.tokenStore(tokenStore)
// .tokenServices(tokenService())//远程验证令牌的服务
.stateless(true);//用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')") //授权范围
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 现在基于token形式,这里就不记录session了
}
}
3、测试
1)申请 jwt
令牌
2)使用令牌请求资源
小技巧:
令牌申请成功可以使用 http://localhost:53020/uaa/oauth/check_token
校验令牌的有效性,并查询令牌的内容,例子如下:
6.4 完善数据库表环境配置
截止目前客户端信息和授权码仍然存储在内存中,生产环境中通过会存储在数据库中,下边完善环境的配置:
6.4.1 创建表
在 user_db数据库
中创建如下表:
oauth_client_details
表,Spring Security OAuth2使用,用来存客户端详情信息:
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '客户端id',
`resource_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '允许访问资源列表',
`client_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '客户端密钥',
`scope` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '授权范围',
`authorized_grant_types` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'client允许的授权类型',
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '回调地址',
`authorities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔',
`access_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的access_token的有效时间值(单位:秒),可选',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的refresh_token的有效时间值(单位:秒),可选',
`additional_information` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '这是一个预留的字段,在Oauth的流程中没有实际的使用,可选',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据的创建时间,精确到秒',
`archived` tinyint(4) DEFAULT NULL COMMENT '用于标识客户端是否已存档(即实现逻辑删除),默认值为''0''(即未存档).',
`trusted` tinyint(4) DEFAULT NULL COMMENT '设置客户端是否为受信任的,默认为''0''(即不受信任的,1为受信任的).',
`autoapprove` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '设置用户是否自动Approval操作, 默认值为 ''false''',
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户端详情服务信息 ';
INSERT INTO `oauth_client_details` VALUES ('c1', 'res1', '$2a$10$Dappn49nM8ZjkCO3ASwR/evnHQN3iPH4npP45js0d9Ug3Vm4ui.sq', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2', 'secret', 'ROLE_API', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '7200', '259200', null, '2020-12-13 15:53:10', '0', '0', 'false');
oauth_code
表,Spring Security OAuth2使用,用来存储授权码:
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '存储服务端系统生成的code的值(未加密).',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)',
`authentication` blob COMMENT '存储将AuthorizationRequestHolder.java对象序列化后的二进制数据.'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='存储授权码';
6.4.2 配置授权服务
(1)distributed-security-uaa
工程授权服务端的 Config
包下 AuthorizationServer
授权类中配置修改 ClientDetailsService
和AuthorizationCodeServices
从数据库读取数据 :
package com.king.security.distributed.uaa.config;
import org.springframework.beans.factory.annotation.Autowired;
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.bcrypt.BCryptPasswordEncoder;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import javax.sql.DataSource;
import java.util.Arrays;
/**
* 授权配置类
* @EnableAuthorizationServer:认证授权服务,提供用于获取token,解析token相关功能,实现认证、授权功能。
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
/**
* 注入检验密码编码器
*/
@Autowired
PasswordEncoder passwordEncoder;
/**
* 将客户端详情服务信息存储到数据库
* @param dataSource
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* 配置客户端详情服务,客户端鉴权配置 client_id :客户端标识、client_secret :客户端秘钥
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 暂时使用内存方式存储客户端详情信息
// clients.inMemory()// 使用in-memory存储
// .withClient("c1")// client_id 客户端ID
// .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
// .resourceIds("res1")// 资源列表
// .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
// .scopes("all")// 允许的授权范围
// .autoApprove(false)// false会跳转到授权页面
// .redirectUris("http://www.baidu.com");// 加上验证回调地址
// .and() // 配置多个客户端用 and 继续配置
// .withClient("c2")// client_id 客户端ID
// .secret(new BCryptPasswordEncoder().encode("secret"))// 客户端密钥
// .resourceIds("res1")// 资源列表
// .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
// .scopes("all")// 允许的授权范围
// .autoApprove(false)// false会跳转到授权页面
// .redirectUris("http://www.zhihu.com");// 加上验证回调地址
// 使用数据库方式存储客户端详情信息
clients.withClientDetails(clientDetailsService);
}
/**
* 令牌管理服务
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客户端详情服务
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
// 令牌增强,生成JWT令牌
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
/**
* 设置授权码模式的授权码
* @param dataSource
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
// 设置授权码模式的授权码如何存取,暂时采用内存方式
// return new InMemoryAuthorizationCodeServices();
// 采用数据库方式
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 用来配置令牌(token)的访问端点和令牌服务(token services)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理器(密码模式需要)
.authorizationCodeServices(authorizationCodeServices)//授权码服务需要
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允信post提交
}
/**
* 用来配置令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")// http://localhost:53010/uaa/oauth/token 获取token令牌这个endpoint端点当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个permitAll()完全公开
.checkTokenAccess("permitAll()")// http://localhost:53020/uaa/oauth/check_token 校验token令牌这个endpoint端点完全公开
.allowFormAuthenticationForClients();// 允许表单认证
}
}
6.4.3测试
1、测试申请令牌
使用密码模式申请令牌,客户端信息需要和数据库中的信息一致。
2、测试授权码模式
生成的授权存储到数据库中。
7.Spring Security实现分布式系统授权
7.1 需求分析
回顾技术方案如下:
1、 distributed-security-uaa
认证服务负责认证授权。
2、所有请求经过网关到达微服务。
3、网关负责鉴权客户端以及请求转发。
4、网关将 token
解析后传给微服务,微服务进行授权。
7.2.注册中心
所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。
本节完成注册中心的搭建,注册中心采用 Eureka
。
1、创建 distributed-security-discovery
注册中心服务为 maven
工程,工程结构:
2、pom.xml
依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 引入父级模块名称 -->
<parent>
<artifactId>distributed-security</artifactId>
<groupId>com.king.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!-- 子模块名称 -->
<artifactId>distributed-security-discovery</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
</project>
3、配置文件
在 resources
中配置 application.yml
:
spring:
application:
name: distributed-discovery
server:
port: 53000 # 启动端口
# eureka 访问地址:http://localhost: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}}
启动类:
package com.king.security.distributed.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServer {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServer.class,args);
}
}
7.3.网关 zuul
网关整合 OAuth2.0
有两种思路,一种是认证服务器生成 jwt令牌
, 所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发。
这里选用第一种。把 API网关
作为 OAuth2.0
的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息(jsonToken
)给微服务,这样下游微服务就不需要关心令牌格式解析以及 OAuth2.0
相关机制了。
API网关
在认证授权体系里主要负责两件事:
(1)作为OAuth2.0的资源服务器角色
,实现接入方权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token
)给微服务
微服务拿到明文token
(明文token
中包含登录用户的身份和权限信息)后也需要做两件事:
(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
7.3.1 创建工程
1、创建 distributed-security-gateway
网关服务为 maven
工程,工程结构:
2、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 引入父级模块名称 -->
<parent>
<artifactId>distributed-security</artifactId>
<groupId>com.king.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!-- 子模块名称 -->
<artifactId>distributed-security-gateway</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<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>
</project>
3、配置文件
配置 application.properties
文件:
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.client.register-with-eureka= true
eureka.client.fetch-registry= true
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
统一认证服务(distributed-security-uaa 授权服务端
)与统一用户服务都是网关下微服务,需要在网关上新增路由配置:
zuul.routes.uaa‐service.stripPrefix = false
zuul.routes.uaa‐service.path = /uaa/**
zuul.routes.user‐service.stripPrefix = false
zuul.routes.user‐service.path = /order/**
上面配置了网关接收的请求 url
若符合 /order/**
表达式,将被被转发至distributed-security-order 资源服务端(统一用户服务)
。
启动类:
package com.king.security.distributed.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayServer {
public static void main(String[] args) {
SpringApplication.run(GatewayServer.class, args);
}
}
7.3.2 token配置
前面也介绍了,资源服务器由于需要验证并解析令牌,往往可以通过在授权服务器暴露 check_token
的 Endpoint
来完成,而我们在授权服务器使用的是对称加密的 jwt
,因此知道密钥即可,资源服务与授权服务本就是对称设计, 那把 distributed-security-uaa
授权服务端的 TokenConfig
令牌配置类拷贝到 distributed-security-gateway
的 config
包下就行 。
package com.king.security.distributed.gateway.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;
/**
* token令牌配置类
*/
@Configuration
public class TokenConfig {
/**
* 签名密钥
*/
private String SIGNING_KEY = "uaa123";
/**
* 令牌储存策略
* @return
*/
// @Bean
// public TokenStore tokenStore() {
// // 使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Bean
public TokenStore tokenStore() {
//JWT令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
/**
* JWT访问令牌转换器(JWT算法)
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}
7.3.3 配置资源服务
在 distributed-security-gateway
的 config
包下定义 ResouceServerConfig
资源服务配类,主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要什么样的权限才能访问某个微服务,如:
package com.king.security.distributed.gateway.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
public class ResouceServerConfig {
/**
* 资源ID
*/
public static final String RESOURCE_ID = "res1";
/**
* uaa资源服务配置
*/
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
/**
* 注入令牌储存策略
*/
@Autowired
private TokenStore tokenStore;
/**
* 资源服务器安全配置器
* @param resources
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore) // 令牌储存策略
.resourceId(RESOURCE_ID) // 资源 id
.stateless(true); // 用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
}
/**
* 配置自定义认证和授权
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uaa/**").permitAll(); // distributed-security-uaa授权工程下的 /uaa/** 所有请求可以访问
}
}
/**
* order资源,uaa资源服务配置
*/
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
/**
* 资源服务器安全配置器
* @param resources
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore) // 令牌储存策略
.resourceId(RESOURCE_ID) // 资源 id
.stateless(true); // 用于指示仅允许对这些资源进行基于令牌的身份验证的标志。
}
/**
* 自定义认证和授权
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')"); // distributed-security-order资源工程下的 /order/** 所有请求,scope要有ROLE_API权限可访问
}
}
//配置其它的资源服务..
}
上面定义了两个微服务的资源,其中:
UAAServerConfig
指定了若请求匹配 /uaa/**
网关不进行拦截。
OrderServerConfig
指定了若请求匹配 /order/**
,也就是访问统一用户服务,接入客户端需要有scope
中包含 read
,并且 authorities(权限)
中需要包含 ROLE_USER
。
由于 res1
这个接入客户端,read
包括 ROLE_ADMIN,ROLE_USER,ROLE_API
三个权限。
7.3.4 安全配置
在 distributed-security-gateway
的 config
包下定义 WebSecurityConfig
spring security安全配置:
package com.king.security.distributed.gateway.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* spring security安全配置
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 安全拦截机制(授权)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // csrf().disable():屏蔽CSRF控制,即spring security不再限制CSRF
.authorizeRequests() //开启登录配置
.antMatchers("/**").permitAll(); // 所有请求可以访问
}
}
7.3.转发明文token给微服务
通过 Zuul
过滤器的方式实现,目的是让下游微服务能够很方便的获取到当前的登录用户信息(明文token
)。
(1)实现 Zuul
前置过滤器,完成当前登录用户信息提取,并放入转发微服务的 request
中:
在 distributed-security-gateway
的 filter
包下定义 AuthFilter
配置:
package com.king.security.distributed.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.king.security.distributed.gateway.common.EncryptUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 自定义token传递拦截
*/
public class AuthFilter extends ZuulFilter {
/**
* 是否执行该过滤器,true代表需要过滤
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤类型
* @return
*/
@Override
public String filterType() {
// 前置过滤器
return "pre";
}
/**
* 过滤顺序
* @return
*/
@Override
public int filterOrder() {
// 优先级,数字越小越优先
return 0;
}
/**
* 过滤器的具体逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
/**
* 1.获取令牌内容
*/
// 上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
// 拿到认证用户身份对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断认证用户身份对象如果不是OAuth2数据格式就不进行解析,直接过滤掉
if(!(authentication instanceof OAuth2Authentication)){
return null;
}
// 将证用户身份对象数据转成OAuth2数据格式
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
// 获取用户认证信息
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
// 取出用户身份信息
String principal = userAuthentication.getName();
/**
* 2.组装明文token,转发给微服务,放入header,名称为json‐token
*/
// 存储用户权限集合
List<String> authoritiesList = new ArrayList<>();
// 从userAuthentication取出权限,放在authorities
userAuthentication.getAuthorities().stream().forEach(c -> authoritiesList.add(((GrantedAuthority) c).getAuthority()));
// 拿到OAuth2Request请求对象
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
// 从OAuth2Request请求对象获取其他请求数据
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
// 将数据类型转成object类型
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
// 判断用户认证信息不为空是,将用户名、用户权限列表放到 jsonToken 中
if(userAuthentication!=null){
jsonToken.put("principal",principal);
jsonToken.put("authorities",authoritiesList);
}
// 把身份信息和权限信息放在json中之后,再通过 Base64 加密后加入到上下文请求对象的http的header头部中,转发给微服务
ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
(2)将 filter
纳入 spring 容器
:
在 distributed-security-gateway
的 config
包下定义 ZuulConfig
配置:
package com.king.security.distributed.gateway.config;
import com.king.security.distributed.gateway.filter.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CorsFilter;
/**
* 配置Zuul网关
*/
@Configuration
public class ZuulConfig {
/**
* 将自定义的前置过滤器加入到Spring容器中
* @return
*/
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
7.4.微服务用户鉴权拦截
当微服务收到 明文token
时,应该怎么鉴权拦截呢?自己实现一个 filter
? 自己解析明文token
,自己定义一套资源访问策略?
能不能适配 Spring Security
呢,是不是突然想起了前面我们实现的 Spring Security
基于 token
认证例子。还拿统一用户服务 distributed-security-order
作为网关下游微服务,对它进行改造,增加微服务用户鉴权拦截功能。
(1)增加测试资源,通过验证后并获取用户信息
在 distributed-security-order
的 OrderController
增加以下endpoint
:
package com.king.security.distributed.order.controller;
import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问此url
public String r1() {
// 因为通过 TokenAuthenticationFilter 类的 doFilterInternal() 方法的逻辑处理,将用户信息添加到 SecurityContextHolder 上下文对象中,这里就可以直接通过该对象获得用户信息
UserDTO user = (UserDTO)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "访问资源1";
}
@GetMapping(value = "/r2")
@PreAuthorize("hasAuthority('p2')")
public String r2() {
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "访问资源2";
}
}
(2)Spring Security配置
开启方法保护,并增加Spring配置策略,除了/login方法不受保护(统一认证要调用),其他资源全部需要认证才能访问。
distributed-security-order
的 config
包下 ResouceServerConfig
配置修改:
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// .antMatchers("/**").access("#oauth2.hasScope('all')") // oauth2授权范围
.antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')") // oauth2授权范围
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 现在基于token形式,这里就不记录session了
}
综合上面的配置,共定义了三个资源了,拥有 p1
权限可以访问 r1
资源,拥有 p2
权限可以访问 r2
资源,只要认证通过就能访问 r3
资源。
(3)定义 filter
拦截 token
,并形成 Spring Security
的 Authentication
对象:
distributed-security-order
的 config
包下定义 TokenAuthenticationFilter
配置类:
package com.king.security.distributed.order.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.king.security.distributed.order.common.EncryptUtil;
import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义每个请求过滤器一次
*/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
/**
* 它能够确保在一次请求中只通过一次filter,而不会重复执行
* @param httpServletRequest
* @param httpServletResponse
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 解析从 Zuul 网关转发过来明文token(header头部中的token)
String token = httpServletRequest.getHeader("json-token");
if(token != null){
// 1.解析token
// 因为 json-token 令牌在 Zuul网关加入到上下文请求对象的http的header头部中时通过Base64加了密,这要解密
String json = EncryptUtil.decodeUTF8StringBase64(token);
// 将token转成json对象
JSONObject jsonObject = JSON.parseObject(json);
// 取出用户身份信息,将 principal的 json转成用户对象,方便Controller层获取用户信息
// UserDTO userDTO = new UserDTO();
// String principal = jsonObject.getString("principal");
// userDTO.setUsername(principal);
UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
// 用户权限
JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
// 将用户权限列表转成数组对象
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
// 2.新建并填充authentication
// 将用户信息和权限添加到用户名密码认证令牌对象中进行校验
// 第一个参数:用户信息;第二参数:权重;第三个参数:用户权限列表
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
// 3.将authentication添加Spring Security安全上下文,Spring Security就会检验授权了
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 执行过滤
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
经过上边的过虑器,资源服务中就可以方便到的获取用户的身份信息:
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
还是三个步骤:
- 解析
token
- 新建并填充
authentication
- 将
authentication
保存进安全上下文剩下的事就交给Spring Security
好了。
7.5.集成测试
本案例测试过程描述:
1、采用 OAuth2.0
的密码模式从 UAA
获取 token
。
2、使用该 token
通过网关访问订单服务的测试资源。
(1)通过网关访问 distributed-security-uaa
的授权及获取令牌,获取 token
。注意端口是53010
,网关的端口。
如授权 endpoint
:
GET http://localhost:53010/uaa/oauth/authorize?response_type=code&client_id=c1
令牌 endpoint
,最后通过授权码模式 code
换取 token
令牌:
POST http://localhost:53010/uaa/oauth/token
(2)使用 token
过网关访问订单服务中的r1-r2
测试资源进行测试。
(3)破坏 token
测试
无 token
测试返回内容:
{
“error”: “unauthorized”,
“error_description”: “Full authentication is required to access this resource”
}
破坏 token
测试返回内容:
{
“error”: “invalid_token”,
“error_description”: “Cannot convert access token to JSON”
}
7.6 扩展用户信息
7.6.1 需求分析
目前 jwt
令牌存储了用户的身份信息、权限信息,网关将token明文
化转发给微服务使用,目前用户身份信息仅包括了用户的账号,微服务还需要用户的 ID
、手机号等重要信息。所以,本案例将提供扩展用户信息的思路和方法,满足微服务使用用户信息的需求。
下边分析 JWT
令牌中扩展用户信息的方案:
在认证阶段 DaoAuthenticationProvider
会调用 UserDetailService
查询用户的信息,这里是可以获取到齐全的用户信息的。由于JWT
令牌中用户身份信息来源于UserDetails
,UserDetails
中仅定义了 username
为用户的身份信息, 这里有两个思路:第一是可以扩展 UserDetails
,使之包括更多的自定义属性,第二也可以扩展 username
的内容 ,比如存入 json
数据内容作为 username
的内容。相比较而言,方案二比较简单还不用破坏UserDetails
的结构,这里采用方案二。
7.6.2 修改UserDetailService
从数据库查询到 user
,将整体 user
转成 json
存入 userDetails
对象。
在 distributed-security-uaa
的 service
包下修改 SpringDataUserDetailsService
配置类:
package com.king.security.distributed.uaa.service;
import com.alibaba.fastjson.JSON;
import com.king.security.distributed.uaa.dao.UserDao;
import com.king.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 自定UserDetailsService用户认证器
*/
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
/**
* 根据用户账号查询信息
* @param username 用户账号
* @return 返回用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//登录账号
System.out.println("username="+username);
//根据账号去数据库查询... //这里暂时使用静态数据写一个用户信息
// UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
// return userDetails;
//根据账号去数据库查询...
UserDto userDto = userDao.getUserByUsername(username);
if(userDto == null){
return null;
}
//查询用户权限
List<String> permissionsList = userDao.findPermissionsByUserId(userDto.getId());
// 定义一个数据
String[] permissionsArray = new String[permissionsList.size()];
// 将 permissionsList 转成 数组
permissionsList.toArray(permissionsArray);
//将userDto转成json
String principal = JSON.toJSONString(userDto);
// 创建userDetails
UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionsArray).build();
return userDetails;
}
}
7.6.3 修改资源服务过虑器
资源服务中的过虑 器负责 从header中解析json-token,从中即可拿网关放入的用户身份信息,部分关键代码如下:
在 distributed-security-order
的 filter
包下修改 TokenAuthenticationFilter
配置类:
package com.king.security.distributed.order.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.king.security.distributed.order.common.EncryptUtil;
import com.king.security.distributed.order.model.UserDTO;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义每个请求过滤器一次
*/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
/**
* 它能够确保在一次请求中只通过一次filter,而不会重复执行
* @param httpServletRequest
* @param httpServletResponse
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 解析从 Zuul 网关转发过来明文token(header头部中的token)
String token = httpServletRequest.getHeader("json-token");
if(token != null){
// 1.解析token
// 因为 json-token 令牌在 Zuul网关加入到上下文请求对象的http的header头部中时通过Base64加了密,这要解密
String json = EncryptUtil.decodeUTF8StringBase64(token);
// 将token转成json对象
JSONObject jsonObject = JSON.parseObject(json);
// 取出用户身份信息,将 principal的 json转成用户对象,方便Controller层获取用户信息
// UserDTO userDTO = new UserDTO();
// String principal = jsonObject.getString("principal");
// userDTO.setUsername(principal);
UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
// 用户权限
JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
// 将用户权限列表转成数组对象
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
// 2.新建并填充authentication
// 将用户信息和权限添加到用户名密码认证令牌对象中进行校验
// 第一个参数:用户信息;第二参数:权重;第三个参数:用户权限列表
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
System.out.println("4------->" + authenticationToken.getPrincipal());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
// 3.将authentication添加Spring Security安全上下文,Spring Security就会检验授权了
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 执行过滤
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
附录
HttpSecurity
HttpSecurity
配置列表:
方法 | 说明 |
---|---|
openidLogin() | 用于基于 OpenId 的验证 |
headers() | 用于基于 OpenId 的验证 |
cors() | 配置跨域资源共享( CORS ) |
sessionManagement() | 允许配置会话管理 |
portMapper() | 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定 向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口 8443,HTTP 端口80到 HTTPS 端口443 |
jee() | 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 |
x509() | 配置基于x509的认证 |
rememberMe | 允许配置“记住我”的验证 |
authorizeRequests() | 允许基于使用HttpServletRequest限制访问 |
requestCache() | 允许配置请求缓存 |
exceptionHandling() | 允许配置错误处理 |
securityContext() | 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将 自动应用 |
servletApi() | 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用 |
csrf() | 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用 |
logout() | 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来 清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success” |
anonymous() | 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用 org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS” |
formLogin() | 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面 |
oauth2Login() | 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 |
requiresChannel() | 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 |
addFilterAt() | 在指定的Filter类的位置添加过滤器 |