# 基本概念
# 什么是认证
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
认证︰用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有∶用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
# 什么是会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于 sessian 方式、基于 token 方式等。
- 基于 session 的认证方式
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在 session (当前会话) 中,发给客户端的 sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或 session 过期销毁时,客户端的 session_id 也就无效了。
- 基于 token 方式
它的交互流程是,用户认证成功后,服务端生成一个 token 发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到 token 通过验证后即可确认用户身份。
# 什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权︰授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
# 授权的数据模型
如何进行授权即如何对用户访问资源进行挤制,首先需要学习授权相关的数据模型
授权可简单理解为 Who 对 What (which) 进行 How 操作,包括如下:
Who,即主体 (Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。
What,即资源 (Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按钮、代码方法都属于系统功能资源,对于 web 系统每个功能资源通常对应一个 URL; 系统商品信息、系统订单信息都属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号为 001 的商品为资源实例。
How,权限 / 许可 (Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为 001 的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。
主体、资源、权限关系如下图:
![Security_1](Spring Security.assets/Security_1.png)
主体、资源、权限相关的数据模型如下:
主体(用户 id、账号、密码、.….)
资源(资源 id、资源名称、访问地耻、.….)
权限(权限 id、权限标识、权限名称、资源 id、...) 角色(角色 id、角色名称、...)
角色和权限关系(角色 id、权限 id、... )
主体 (用户)和角色关系(用户 id、角色 id、...)
主体(用户)、资源、权限关系如下图:
![Security_2](Spring Security.assets/Security_2.png)
# RBAC
基于角色的访问控制
基于资源的访问控制
# 基于 Session 的认证方式
# 认证流程
基于 Session 的认证机制由 Servlet 规范定制,Servlet 容器已实现,用户通过 HttpSession 的操作方法即可实现,如下是 HttpSession 相关的操作 APl。
方法 | 含义 |
---|---|
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 失效 |
... | ... |
# 创建工程
# 创建 maven 工程
创建 maven 工程 security-springmvc,工程结构如下
![security_3 ](Spring Security.assets/security_3 .png)
导入依赖
<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> |
# Spring 容器配置
在 config 包下定义 ApplicationConfig.java,它对应 web.xml 中 ContextLoaderLister 的配置
@Configuration // 相当于 applicationContext.xml | |
@ComponentScan(basePackages = "com.example.demo.springmvc", | |
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) | |
public class ApplicationConfig { | |
// 在此配置处理 COntroller 的其他 bean,比如数据库连接池、事务管理器、业务 bean 等。 | |
} |
# servletContext 配置
在 config 包下定义 WebConfig,它对应 DispatcherServlet 配置
@Configuration // 相当于 springmvc.xml 文件 | |
@EnableWebMvc | |
@ComponentScan(basePackages = "com.example.demo.springmvc", | |
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) | |
public class WebConfig implements WebMvcConfigurer { | |
// 视图解析器 | |
@Bean | |
public InternalResourceViewResolver viewResolver(){ | |
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); | |
viewResolver.setPrefix("/WEB-INF/view/"); | |
viewResolver.setSuffix(".jsp"); | |
return viewResolver; | |
} | |
} |
# 加载 Spring 容器
在 init 包下定义 Spring 容器初始化类 SpringApplicationInitializer,此类实现 WenApplicationInitializer 接口,Spring 容器启动时加载 WenApplicationInitializer 接口的所有实现类
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { | |
//spring 容器,相当于 applicationContextmvc.xml | |
@Override | |
protected Class<?>[] getRootConfigClasses() { | |
return new Class[]{ApplicationConfig.class}; | |
} | |
//servletConetxt,相当于加载 springmvc.xml | |
@Override | |
protected Class<?>[] getServletConfigClasses() { | |
return new Class[]{WebConfig.class}; | |
} | |
//url-mapping | |
@Override | |
protected String[] getServletMappings() { | |
return new String[]{"/"}; | |
} | |
} |
# 实现认证功能
# 认证页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="login" method="post">
账号:<input type="text" name="username"> <br>
密码:<input type="text" name="password"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
在 WebConfig 中新增如下配置,将 / 直接导向 login.jsp 页面
... | |
public class WebConfig implements WebMvcConfigurer { | |
... | |
@Override | |
public void addViewControllers(ViewControllerRegistry registry) { | |
registry.addViewController("/").setViewName("login"); | |
} | |
} |
配置 maven 或者 tomcat 启动项目,测试
# 认证接口
用户进入认证页面,输入账号和密码,点击登录,请求 /login 进行身份认证
- 定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常
public interface AuthenticationService { | |
/** | |
* 用户认证 | |
* @param authenticationRequest 用户认证请求,账号和密码 | |
* @return 认证成功的用户信息 | |
*/ | |
UserDto authentication(AuthenticationRequest authenticationRequest); | |
} |
认证请求结构
@Data | |
public class AuthenticationRequest { | |
// 认证请求参数,账号密码 | |
/** | |
* 用户名 | |
*/ | |
private String username; | |
/** | |
* 密码 | |
*/ | |
private String password; | |
} |
认证成功后返回的用户详细信息,也就是当前登录用户的信息
@Data | |
@AllArgsConstructor | |
public class UserDto { | |
// 用户身份信息 | |
private String id; | |
private String username; | |
private String password; | |
private String fullname; | |
private String mobile; | |
} |
- 认证实现类,根据用户名查找用户信息,并校验密码,这里模拟了两个用户
@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 userDto = getUserDto(authenticationRequest.getUsername()); | |
// 判断用户是否为空 | |
if (userDto == null){ | |
throw new RuntimeException("查询不到该用户"); | |
} | |
// 校验密码 | |
if (!authenticationRequest.getPassword().equals(userDto.getPassword())){ | |
throw new RuntimeException("账号或密码错误"); | |
} | |
// 认证通过返回用户信息 | |
return userDto; | |
} | |
/** | |
* 模拟用户查询 | |
*/ | |
public 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")); | |
} | |
} |
- 登录 Controller,对 /login 请求处理,它调用 AuthenticationService 完成认证并返回登录结果提示信息
@RestController | |
public class LoginController { | |
@Autowired | |
private AuthenticationService authenticationService; | |
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8") | |
public String login(AuthenticationRequest authenticationRequest){ | |
UserDto userDto = authenticationService.authentication(authenticationRequest); | |
return userDto.getUsername() +"登录成功"; | |
} | |
} |
- 测试
启动项目,访问 / 路径地址,进行测试
填入错误的用户信息,页面返回错误信息
填入正确的用户信息,页面提示登录成功
# 实现会话功能
会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。 认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性 拦截。因此,在认证成功后,一般会把认证成功的用户信息放入 Session 中,在后续的请求中,系统能够从 Session 中获取到当前用户,用这样的方式来实现会话机制。
(1)增加会话控制 首先在 UserDto 中定义一个 SESSION_USER_KEY,作为 Session 中存放登录用户信息的 key。
public class UserDto { | |
public static final String SESSION_USER_KEY = "_user"; | |
... | |
} |
然后修改 LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将 session 置为 失效。
@RestController | |
public class LoginController { | |
@Autowired | |
private 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); | |
return userDto.getUsername() +"登录成功"; | |
} | |
@RequestMapping(value = "louOut",produces = "text/plain;charset=utf-8") | |
public String logout(HttpSession session) { | |
session.invalidate(); | |
return "退出成功"; | |
} | |
} |
(2)增加测试资源 修改 LoginController,增加测试资源 1,它从当前会话 session 中获取当前登录用户,并返回提示信息给前台。
@RestController | |
public class LoginController { | |
... | |
@RequestMapping(value = "/test1",produces = "text/plain;charset=utf-8") | |
public String test1(HttpSession session){ | |
String fullname = null; | |
Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY); | |
if (userObj != null){ | |
fullname = ((UserDto) userObj).getFullname(); | |
}else { | |
fullname = "匿名"; | |
} | |
return fullname+"访问资源1"; | |
} | |
} |
(3)测试
未登录情况下直接访问测试资源
匿名访问资源 1
成功登录的情况下访问测试资源
张三访问资源 1
# 实现授权功能
现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户 (从 Session 中获取) 的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:
- 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
- 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。
- 增加权限数据 为了实现这样的功能,我们需要在 UserDto 里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto 的构造方法。
@Data | |
@AllArgsConstructor | |
public class UserDto { | |
... | |
/** | |
* 用户权限 | |
*/ | |
private Set<String> authorities; | |
} |
并在 AuthenticationServiceImpl 中为模拟用户初始化权限,其中张三给了 p1 权限,李四给了 p2 权限。
@Service | |
public class AuthenticationServiceImpl implements AuthenticationService{ | |
... | |
// 用户信息 | |
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)); | |
} | |
} |
- 增加测试资源 我们想实现针对不同的用户能访问不同的资源,前提是得有多个资源,因此在 LoginController 中增加测试资源 2
@RestController | |
public class LoginController { | |
... | |
@RequestMapping(value = "/test2",produces = "text/plain;charset=utf-8") | |
public String test2(HttpSession session){ | |
String fullname = null; | |
Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY); | |
if (userObj != null){ | |
fullname = ((UserDto) userObj).getFullname(); | |
}else { | |
fullname = "匿名"; | |
} | |
return fullname+"访问资源2"; | |
} | |
} |
- 实现授权拦截器 在 interceptor 包下定义 SimpleAuthenticationInterceptor 拦截器,实现授权拦截:
1、校验用户是否登录
2、校验用户是否拥有操作权限
@Component | |
public class SimpleAuthenticationInterceptor implements HandlerInterceptor { | |
@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){ | |
wirteContent(response,"请登录"); | |
} | |
UserDto userDto = (UserDto) object; | |
// 请求的 url | |
String requestURI = request.getRequestURI(); | |
if (userDto.getAuthorities().contains("p1") && requestURI.contains("/test1")) { | |
return true; | |
} | |
if (userDto.getAuthorities().contains("p2") && requestURI.contains("/test2")) { | |
return true; | |
} | |
wirteContent(response,"没有权限,拒绝访问"); | |
return false; | |
} | |
// 响应信息给客户端 | |
private void wirteContent(HttpServletResponse response,String msg) throws IOException{ | |
response.setContentType("text/html;charset=utf-8"); | |
PrintWriter writer = response.getWriter(); | |
writer.print(msg); | |
writer.close(); | |
response.resetBuffer(); | |
} | |
} |
在 WebConfig 中配置拦截器,匹配 /test1,/test2 的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor 拦截器。
public class WebConfig implements WebMvcConfigurer { | |
... | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
registry.addInterceptor(authenticationInterceptor). | |
addPathPatterns("/test1"). | |
addPathPatterns("/test2"); | |
} | |
} |
- 测试
未登录情况下,访问 /test1,/test2
请登录
登录张三的账号,访问 /test1
张三访问资源 1
访问 /test2
没有权限,拒绝访问
登录李四的账号,只能访问 /test2,不能访问 /test1
# Spring Security 快速上手
# Spring Security 介绍
Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是 Spring 生态系统中的一员,因此它伴随着整个 Spring 生态系统不断修正、升级,在 spring boot 项目中加入 spring security 更是十分简单,使用 Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。
# 创建工程
# 创建 maven 工程
创建 maven 工程 spring-security,工程结构如下
![security_4](Spring Security.assets/security_4.png)
引入以下依赖
<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.security</groupId> | |
<artifactId>spring-security-web</artifactId> | |
<version>5.1.5.RELEASE</version> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.security</groupId> | |
<artifactId>spring-security-config</artifactId> | |
<version>5.3.6.RELEASE</version> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-webmvc</artifactId> | |
<version>5.1.5.RELEASE</version> | |
</dependency> | |
<dependency> | |
<groupId>javax.servlet</groupId> | |
<artifactId>javax.servlet-api</artifactId> | |
<version>3.0.1</version> | |
<scope>provided</scope> | |
</dependency> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
<version>1.18.8</version> | |
</dependency> | |
</dependencies> | |
<build> | |
<finalName>security-springmvc</finalName> | |
<pluginManagement> | |
<plugins> | |
<plugin> | |
<groupId>org.apache.tomcat.maven</groupId> | |
<artifactId>tomcat7-maven-plugin</artifactId> | |
<version>2.2</version> | |
</plugin> | |
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-compiler-plugin</artifactId> | |
<configuration> | |
<source>1.8</source> | |
<target>1.8</target> | |
</configuration> | |
</plugin> | |
<plugin> | |
<artifactId>maven-resources-plugin</artifactId> | |
<configuration> | |
<encoding>utf-8</encoding> | |
<useDefaultDelimiters>true</useDefaultDelimiters> | |
<resources> | |
<resource> | |
<directory>src/main/resources</directory> | |
<filtering>true</filtering> | |
<includes> | |
<include>**/*</include> | |
</includes> | |
</resource> | |
<resource> | |
<directory>src/main/java</directory> | |
<includes> | |
<include>**/*.xml</include> | |
</includes> | |
</resource> | |
</resources> | |
</configuration> | |
</plugin> | |
</plugins> | |
</pluginManagement> | |
</build> |
# Spring 容器配置
@Configuration // 相当于 applicationContext.xml | |
@ComponentScan(basePackages = "com.example.demo.springmvc", | |
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) | |
public class ApplicationConfig { | |
// 在此配置处理 Controller 的其他 bean,比如数据库连接池、事务管理器、业务 bean 等。 | |
} |
# Servlet Context 配置
@Configuration // 相当于 springmvc.xml 文件 | |
@EnableWebMvc | |
@ComponentScan(basePackages = "com.example.demo.springmvc", | |
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) | |
public class WebConfig implements WebMvcConfigurer { | |
// 视图解析器 | |
@Bean | |
public InternalResourceViewResolver viewResolver(){ | |
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); | |
viewResolver.setPrefix("/WEB-INF/view/"); | |
viewResolver.setSuffix(".jsp"); | |
return viewResolver; | |
} | |
@Override | |
public void addViewControllers(ViewControllerRegistry registry) { | |
registry.addViewController("/").setViewName("login"); | |
} | |
} |
# 加载 Spring 容器
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { | |
//spring 容器,相当于 applicationContextmvc.xml | |
@Override | |
protected Class<?>[] getRootConfigClasses() { | |
return new Class[]{ApplicationConfig.class}; | |
} | |
//servletConetxt,相当于加载 springmvc.xml | |
@Override | |
protected Class<?>[] getServletConfigClasses() { | |
return new Class[]{WebConfig.class}; | |
} | |
//url-mapping | |
@Override | |
protected String[] getServletMappings() { | |
return new String[]{"/"}; | |
} | |
} |
# 认证
# 认证页面
springSecurity 默认提供认证页面,不需要额外开发。
# 安全配置
spring security 提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
- 在 config 包下定义 WebSecurityConfig,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
@EnableWebSecurity | |
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { | |
// 定义用户信息服务(查询用户信息) | |
@Bean | |
public UserDetailsService userDetailsService(){ | |
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); | |
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); | |
manager.createUser(User.withUsername("lisi").password("123").authorities("p2").build()); | |
return manager; | |
} | |
// 密码编译器 | |
@Bean | |
public PasswordEncoder passwordEncoder(){ | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
// 安全拦截机制 | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.authorizeRequests() | |
.antMatchers("/test1").authenticated() // test1 的请求必须认证通过 | |
.anyRequest().permitAll() // 除了 /test1,其他的请求都可以访问 | |
.and() | |
.formLogin() // 允许表单登录 | |
.successForwardUrl("/login_success"); // 自定义登录成功的页面地址 | |
} | |
} |
在 userDetailsService () 方法中,我们返回了一个 UserDetailsService 给 spring 容器,Spring Security 会使用它来 获取用户信息。我们暂时使用 InMemoryUserDetailsManager 实现类,并在其中分别创建了 zhangsan、lisi 两个用 户,并设置密码和权限。
- 加载 WebSecurityConfig 修改 SpringApplicationInitializer 的 getRootConfigClasses () 方法,添加 WebSecurityConfig.class
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { | |
//spring 容器,相当于 applicationContextmvc.xml | |
@Override | |
protected Class<?>[] getRootConfigClasses() { | |
return new Class[]{ApplicationConfig.class,WebSecurityConfig.class}; | |
} | |
... | |
} |
# Spring Security 初始化
Spring Security 初始化,这里有两种情况
- 若当前环境没有使用 Spring 或 Spring MVC,则需要将 WebSecurityConfig (Spring Security 配置类) 传入超 类,以确保获取配置,并创建 spring context。
- 相反,若当前环境已经使用 spring,我们应该在现有的 springContext 中注册 Spring Security (上一步已经做将 WebSecurityConfig 加载至 rootcontext),此方法可以什么都不做。 在 init 包下定义 SpringSecurityApplicationInitializer:
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { | |
public SpringSecurityApplicationInitializer() { | |
} | |
} |
# 默认根路径请求
在 WebConfig.java 中添加默认请求根路径跳转到 /login,此 url 为 spring security 提供
public class WebConfig implements WebMvcConfigurer { | |
... | |
@Override | |
public void addViewControllers(ViewControllerRegistry registry) { | |
registry.addViewController("/").setViewName("redirect:/login"); | |
} | |
} |
spring security 默认提供的登录页面
# 认证成功页面
在安全配置中,认证成功将跳转到 /login-success
spring security 支持 form 表单认证,认证成功后转向 /login-success。 在 LoginController 中定义 /login-success
@RestController | |
public class LoginController { | |
@RequestMapping(value = "/login_success",produces = {"text/plain;charset=UTF-8"}) | |
public String loginSuccess(){ | |
return " 登录成功"; | |
} | |
@GetMapping(value = "/test1",produces = {"text/plain;charset=UTF-8"}) | |
public String test1(){ | |
return " 访问资源1"; | |
} | |
} |
# 测试
不登录请求 /test1,会自动跳回登录界面
输入错误的账号密码,出现账号密码错误
输入正确的账号密码,登出成功,访问 /test1
访问资源 1
# 授权
实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security 默认提供授 权实现方法。 在 LoginController 添加 /t/test2,/t/test2
@RestController | |
public class LoginController { | |
... | |
@RequestMapping(value = "/t/test1",produces = {"text/plain;charset=UTF-8"}) | |
public String test1(){ | |
return " 访问资源1"; | |
} | |
@RequestMapping(value = "/t/test2",produces = {"text/plain;charset=UTF-8"}) | |
public String test2(){ | |
return " 访问资源2"; | |
} | |
} |
在安全配置类 WebSecurityConfig.java 中配置授权规则
.antMatchers ("/text1").hasAuthority ("p1") 表示:访问 /test1 资源的 url 需要拥有 p1 权限。
.antMatchers ("/text2").hasAuthority ("p2") 表示:访问 /test2 资源的 url 需要拥有 p2 权限。
@EnableWebSecurity | |
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
// 安全拦截机制 | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.authorizeRequests() | |
.antMatchers("/t/test1").hasAuthority("p1") | |
.antMatchers("/t/test2").hasAuthority("p2") | |
.antMatchers("/t/**").authenticated() // /t 以下的请求必须认证通过 | |
.anyRequest().permitAll() // 除了 /t 以下,其他的请求都可以访问 | |
.and() | |
.formLogin() // 允许表单登录 | |
.successForwardUrl("/login_success"); // 自定义登录成功的页面地址 | |
} | |
} |
测试
登录张三的账号访问 /test1
访问资源 1
访问 /test2
出现 403,说明没有权限
# Spring Security 应用详解
# 集成 SpringBoot
# 创建 maven 工程
创建 springboot 工程 security-spring-boot,工程结构如下
导入依赖
# Servlet Context 配置
由于 Spring boot starter 自动装配机制,这里无需使用 @EnableWebMvc 与 @ComponentScan,WebConfig 如下
@Configuration // 相当于 springmvc.xml 文件 | |
public class WebConfig implements WebMvcConfigurer { | |
@Override | |
public void addViewControllers(ViewControllerRegistry registry) { | |
registry.addViewController("/").setViewName("redirect:/login"); | |
} | |
} |
# 安全配置
由于 Spring boot starter 自动装配机制,这里无需使用 @EnableWebSecurity,WebSecurityConfig 内容如下
@Configuration | |
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { | |
// 定义用户信息服务(查询用户信息) | |
@Bean | |
public UserDetailsService userDetailsService(){ | |
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); | |
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); | |
manager.createUser(User.withUsername("lisi").password("123").authorities("p2").build()); | |
return manager; | |
} | |
// 密码编译器 | |
@Bean | |
public PasswordEncoder passwordEncoder(){ | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
// 安全拦截机制 | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.authorizeRequests() | |
.antMatchers("/t/test1").hasAuthority("p1") | |
.antMatchers("/t/test2").hasAuthority("p2") | |
.antMatchers("/t/**").authenticated() // test1,test2 的请求必须认证通过 | |
.anyRequest().permitAll() // 除了 /test1,/test2,其他的请求都可以访问 | |
.and() | |
.formLogin() // 允许表单登录 | |
.successForwardUrl("/login_success"); // 自定义登录成功的页面地址 | |
} | |
} |
# 测试
LoginController
@RestController | |
public class LoginController { | |
@RequestMapping(value = "/login_success",produces = {"text/plain;charset=UTF-8"}) | |
public String loginSuccess(){ | |
return " 登录成功"; | |
} | |
@RequestMapping(value = "/t/test1",produces = {"text/plain;charset=UTF-8"}) | |
public String test1(){ | |
return " 访问资源1"; | |
} | |
@RequestMapping(value = "/t/test2",produces = {"text/plain;charset=UTF-8"}) | |
public String test2(){ | |
return " 访问资源2"; | |
} | |
} |
运行 springboot 项目进行测试
# 工作原理
# 结构总览
Spring Security 所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过 Filter 或 AOP 等技术来实现,Spring Security 对 Web 资源的保护是靠 Filter 实现的,所以从这个 Filter 来入手,逐步深入 Spring Security 原理。
当初始化 Spring Security 时,会创建一个名为 SpringSecurityFilterChain 的 Servlet 过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了 javax.servlet.Filter,因此外部的请求会经过此 类
FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain 所包含的各个 Filter,同时 这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理
spring Security 功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
- SecurityContextPersistenceFilter 这个 Filter 是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
- UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
- FilterSecurityInterceptor 是用于保护 web 资源的,使用 AccessDecisionManager 对当前用户进行授权访问,前 面已经详细介绍过了;
- ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
# 认证流程
# 认证流程
![security_5](Spring Security.assets/security_5.png)
- 用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
- 然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext ().setAuthentication (…) 方法,设置到其中。 可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为 ProviderManager。而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider 完成的。咱们知道 web 表单的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService 负责 UserDetails 的获取。最终 AuthenticationProvider 将 UserDetails 填充至 Authentication。
# 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。如使用用户名密码登录时,使用 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 就是当前登录用户信息,它也是框架中的常用接口之一。
# UserDetailsService
- 认识 UserDetailsService
现在咱们现在知道 DaoAuthenticationProvider 处理了 web 表单的认证逻辑,认证成功后既得到一个 Authentication (UsernamePasswordAuthenticationToken 实现),里面包含了身份信息(Principal)。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为 UserDetails 对象。
DaoAuthenticationProvider 中包含了一个 UserDetailsService 实例,它负责根据用户名提取用户信息 UserDetails (包含密码),而后 DaoAuthenticationProvider 会去对比 UserDetailsService 提取的用户密码与用户提交 的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为 spring bean 来定 义自定义身份验证。
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 的实现类,主要区别无非就是从内存还是从数据库加载用户。
- 测试
自定义 UserDetailsService
@Service | |
public class SpringDataUserDetailsService implements UserDetailsService { | |
// 根据账号查询用户信息 | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
System.out.println(username); | |
UserDetails userDetails = User.withUsername("zhangsan").password("123").authorities("p1").build(); | |
return userDetails; | |
} | |
} |
将安全配置类 WebSecurityCondig 中 UserDetailsService 的注释掉
@Configuration | |
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { | |
/* | |
// 定义用户信息服务(查询用户信息) | |
@Bean | |
public UserDetailsService userDetailsService (){ | |
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager (); | |
manager.createUser (User.withUsername ("zhangsan").password ("123").authorities ("p1").build ()); | |
manager.createUser (User.withUsername ("lisi").password ("123").authorities ("p2").build ()); | |
return manager; | |
}*/ | |
... | |
} |
重启项目,请求认证,SpringDataUserDetailsService 的 loadUserByUsername 方法被调用 ,查询用户信息。
# PasswordEncoder
- 认识 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 采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1、用户输入密码(明文 )
2、DaoAuthenticationProvider 获取 UserDetails(其中存储了用户的正确密码)
3、DaoAuthenticationProvider 使用 PasswordEncoder 对输入的密码和正确的密码进行校验,密码一致则校验通 过,否则校验失败。
NoOpPasswordEncoder 的校验规则拿 输入的密码和 UserDetails 中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则 校验失败。
实际项目中推荐使用 BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder 等,感兴趣 的大家可以看看这些 PasswordEncoder 的具体实现。
- 使用 BCryptPasswordEncoder
1、配置 BCryptPasswordEncode
在安全配置类中定义
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return new BCryptPasswordEncoder(); | |
} |
# SpringSecurity Web 权限方案
# 新建 SpringBoot 工程
项目结构
![](Spring Security.assets/security_6-1610440819742.png)
选择依赖
lombok,SpringWeb,Thymeleaf、Spring Security、MySQL Driver
新建 TestController
@RestController | |
public class TestController { | |
@RequestMapping("/test") | |
public String test(){ | |
return "Hello"; | |
} | |
} |
启动 SpringBoot 项目,当不设置账号密码时,控制台会有输出默认的账号密码
访问 /test 会自动跳回登录页面,登录成功之后在访问 /test
# 设置登录系统的账号、密码
方式一:在 application.yml
spring: | |
security: | |
user: | |
name: zhangsan | |
password: 123456 |
方式二:编写配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); | |
String password = passwordEncoder.encode("123456"); | |
auth.inMemoryAuthentication().withUser("zhangsan").password(password).roles("admin"); | |
} | |
@Bean | |
PasswordEncoder password(){ | |
return new BCryptPasswordEncoder(); | |
} | |
} |
方式三:编写类实现接口
第 — 步创建配置类,设置使用哪个 userDetailsService 实现类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Autowired | |
private UserDetailsService userDetailsService; | |
@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
auth.userDetailsService(userDetailsService).passwordEncoder(password()); | |
} | |
@Bean | |
PasswordEncoder password(){ | |
return new BCryptPasswordEncoder(); | |
} | |
} |
第二步编写实现类,返回 User 对象,User 对象有用户名密码和操作权限
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Override | |
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); | |
return new User("zhangsan",new BCryptPasswordEncoder().encode("123456"),auths); | |
} | |
} |
# 实现数据库认证来完成用户登录
# 准备 sql
create table users( | |
id bigint primary key auto_increment, | |
username varchar(20) unique not null, | |
password varchar(100) | |
); | |
insert into users values(1,'zhangsan','123456'); | |
insert into users values(2,'lisi','123456'); | |
create table role( | |
id bigint primary key auto_increment, | |
name varchar(20) | |
); | |
insert into role values(1,'管理员'); | |
insert into role values(2,'普通用户'); | |
create table role_user( | |
uid bigint, | |
rid bigint | |
); | |
insert into role_user values(1,1); | |
insert into role_user values(2,2); | |
create table menu( | |
id bigint primary key auto_increment, | |
name varchar(20), | |
url varchar(100), | |
parentid bigint, | |
permission varchar(20) | |
); | |
insert into menu values(1,'系统管理','',0,'menu:system'); | |
insert into menu values(2,'用户管理','',0,'menu:user');\ | |
create table role_menu( | |
mid bigint, | |
rid bigint | |
); | |
insert into role_menu values(1,1); | |
insert into role_menu values(2,1); | |
insert into role_menu values(2,2); |
# 添加依赖
<!--mybatis-plus--> | |
<dependency> | |
<groupId>com.baomidou</groupId> | |
<artifactId>mybatis-plus-boot-starter</artifactId> | |
<version>3.0.5</version> | |
</dependency> |
# 制作实体类
@Data | |
public class Users { | |
private Integer id; | |
private String username; | |
private String password; | |
} |
# 整合 MybatisPlus 制作 mapper
@Repository | |
public interface UserMapper extends BaseMapper<Users> { | |
} |
# 制作登录实现类
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UserMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { | |
// 调用 userMapper 方法,根据用户名查询数据库 | |
QueryWrapper<Users> wrapper = new QueryWrapper<>(); | |
wrapper.eq("username",userMapper); | |
Users users = userMapper.selectOne(wrapper); | |
if (users == null){ | |
throw new UsernameNotFoundException("用户名不存在"); | |
} | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); | |
// 查询数据库返回 user 对象,得到用户名和密码返回 | |
return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths); | |
} | |
} |
# 在启动类添加注解
@SpringBootApplication | |
@MapperScan("com.example.demo.mapper") | |
public class DemoApplication { | |
public static void main(String[] args) { | |
SpringApplication.run(DemoApplication.class, args); | |
} | |
} |
# 配置数据库连接信息
spring: | |
datasource: | |
driver-class-name: com.mysql.cj.jdbc.Driver | |
url: jdbc:mysql://localhost:3306?sercurity?serverTimezone=GMT | |
username: root | |
password: 123456 |
# 测试
# 自定义登录页面
# 新建 login.html 页面
<form action="/user/login" method="post"> | |
账号:<input type="text" name="username"> | |
<br> | |
密码:<input type="text" name="password"> | |
<br> | |
<input type="submit" value="登录"> | |
</form> |
# 在配置类实现相关配置
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Autowired | |
private UserDetailsService userDetailsService; | |
@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
auth.userDetailsService(userDetailsService).passwordEncoder(password()); | |
} | |
@Bean | |
PasswordEncoder password(){ | |
return new BCryptPasswordEncoder(); | |
} | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
# 修改 TestController 内容
@RestController | |
@RequestMapping("/test") | |
public class TestController { | |
@RequestMapping("/hello") | |
public String hello(){ | |
return "hello security"; | |
} | |
@RequestMapping("/index") | |
public String test(){ | |
return "Hello World"; | |
} | |
} |
# 测试
访问 /test/hello,出现 hello security
访问 /test/index,跳回登录页面,登录之后再次访问,出现 Hello World
# 基于角色或权限进行访问控制
# hasAuthority 方法
如果当前的主体具有指定的权限,则返回 true, 否则返回 false
- 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasAuthority("admin") | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
- 修改 MyUserDetailsService 类
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
// 调用 userMapper 方法,根据用户名查询数据库 | |
QueryWrapper<Users> wrapper = new QueryWrapper<>(); | |
wrapper.eq("username",username); | |
Users users = userMapper.selectOne(wrapper); | |
if (users == null){ | |
throw new UsernameNotFoundException("用户名不存在"); | |
} | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("employee"); | |
// 查询数据库返回 user 对象,得到用户名和密码返回 | |
return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths); | |
} | |
} |
- 测试
启动项目,登录成功之后访问 /test/index
出现 403,表示没有权限
- 修改修改 MyUserDetailsService 类
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
... | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin"); | |
... | |
} | |
} |
- 测试
启动项目,登录成功之后访问 /test/index
出现 Hello World
# hasAnyAuthority 方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true.
- 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasAnyAuthority("admin,manager") | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
- 测试
登录成功之后可访问
- 修改修改 MyUserDetailsService 类
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
... | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("manager"); | |
... | |
} | |
} |
- 测试
登录成功之后可访问
# hasRole 方法
如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true。
底层源码
private static String hasRole(String role) { | |
Assert.notNull(role, "role cannot be null"); | |
Assert.isTrue(!role.startsWith("ROLE_"), () -> { | |
return "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'"; | |
}); | |
return "hasRole('ROLE_" + role + "')"; | |
} |
- 给用户添加角色
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
... | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale"); | |
... | |
} | |
} |
- 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasRole("sale") | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
- 测试
# hasAnyRole 方法
表示用户具备任何一个条件都可以访问。
- 给用户添加角色
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
... | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale1,ROLE_sale2"); | |
... | |
} | |
} |
- 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasAnyRole("sale1","sale2") | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
- 测试
# 基于数据库实现权限认证
# 添加实体类
@Data | |
public class Role { | |
private Long id; | |
private String name; | |
} |
@Data | |
public class Menu { | |
private Long id; | |
private String name; | |
private String url; | |
private Long parentId; | |
private String permission; | |
} |
# 编写接口与实现类
@Repository | |
public interface UserInfoMapper { | |
/** | |
* 根据用户 Id 查询用户角色 | |
* @param userId | |
* @return | |
*/ | |
@Select("SELECT r.id,r.name FROM role r INNER JOIN role_user ru ON " + | |
"ru.rid=r.id where ru.uid=#{userId}") | |
List<Role> selectRoleByUserId(Integer userId); | |
/** | |
* 根据用户 Id 查询菜单 | |
* @param userId | |
* @return | |
*/ | |
@Select("SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m " + | |
" INNER JOIN role_menu rm ON m.id=rm.mid " + | |
"INNER JOIN role r ON r.id=rm.rid " + | |
"INNER JOIN role_user ru ON r.id=ru.rid " + | |
"WHERE ru.uid=#{userId}") | |
List<Menu> selectMenuByUserId(Integer userId); | |
} |
# 修改 MyUserDetailsService
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Autowired | |
private UserInfoMapper userInfoMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
// 调用 userMapper 方法,根据用户名查询数据库 | |
QueryWrapper<Users> wrapper = new QueryWrapper<>(); | |
wrapper.eq("username",username); | |
Users users = userMapper.selectOne(wrapper); | |
if (users == null){ | |
throw new UsernameNotFoundException("用户名不存在"); | |
} | |
// 查询用户名角色菜单列表 | |
List<Role> roleList = userInfoMapper.selectRoleByUserId(users.getId()); | |
List<Menu> menuList = userInfoMapper.selectMenuByUserId(users.getId()); | |
// 声明一个集合 List<GrantedAuthority> | |
List<GrantedAuthority> auths = new ArrayList<>(); | |
// 处理角色 | |
for (Role role : roleList){ | |
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+role.getName()); | |
auths.add(simpleGrantedAuthority); | |
} | |
// 处理权限 | |
for (Menu menu : menuList){ | |
auths.add(new SimpleGrantedAuthority(menu.getPermission())); | |
} | |
//List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale"); | |
// 查询数据库返回 user 对象,得到用户名和密码返回 | |
return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths); | |
} | |
} |
# 修改 TestController
@RestController | |
@RequestMapping("/test") | |
public class TestController { | |
... | |
@RequestMapping("/find") | |
public String find(){ | |
return "find"; | |
} | |
} |
# 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/success.html").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasRole("sale") | |
.antMatchers("/test/find").hasAnyAuthority("menu:system") | |
.anyRequest().authenticated() | |
.and().rememberMe().tokenRepository(persistentTokenRepository()) | |
.tokenValiditySeconds(60) // 设置有效时长,单位秒 | |
.userDetailsService(userDetailsService) | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
# 测试
# 自定义 403 页面
- 新建 unauth 页面
<h1>没有权限访问</h1> |
- 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
... | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
// 配置没有权限访问跳转自定义页面 | |
http.exceptionHandling().accessDeniedPage("/unauth.html"); | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasRole("test") | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
测试,登录之后将会跳转到自定义的 403 页面
# 注解使用
# @Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 “ROLE_“。
使用注解先要开启注解功能!
- 修改启动类
@SpringBootApplication | |
@MapperScan("com.example.demo.mapper") | |
@EnableGlobalMethodSecurity(securedEnabled = true) | |
public class DemoApplication { | |
public static void main(String[] args) { | |
SpringApplication.run(DemoApplication.class, args); | |
} | |
} |
- 修改 TestController
@RestController | |
@RequestMapping("/test") | |
public class TestController { | |
... | |
@RequestMapping("/update") | |
@Secured({"ROLE_sale","ROLE_manager"}) | |
public String update(){ | |
return "update"; | |
} | |
} |
- 修改 MyUserDetailsService
@Service | |
public class MyUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UsersMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
... | |
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale"); | |
... | |
} | |
} |
- 测试
不登录访问 /test/update,自动跳回登录页面
登录之后访问 /test/update 出现 update
# @PreAuthorize
先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用 户的 roles/permissions 参数传到方法中。
- 修改启动类
@SpringBootApplication | |
@MapperScan("com.example.demo.mapper") | |
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) | |
public class DemoApplication { | |
public static void main(String[] args) { | |
SpringApplication.run(DemoApplication.class, args); | |
} | |
} |
- 修改 TestController
@RestController | |
@RequestMapping("/test") | |
public class TestController { | |
... | |
@RequestMapping("/update") | |
//@Secured({"ROLE_sale","ROLE_manager"}) | |
@PreAuthorize("hasAnyAuthority('admin')") | |
public String update(){ | |
return "update"; | |
} | |
} |
- 测试
# @PostAuthorize
先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.
- 修改 TestController
@RestController | |
@RequestMapping("/test") | |
public class TestController { | |
... | |
@RequestMapping("/update") | |
//@Secured({"ROLE_sale","ROLE_manager"}) | |
//@PreAuthorize("hasAnyAuthority('admin')") | |
@PostAuthorize("hasAnyAuthority('admins')") | |
public String update(){ | |
System.out.println("update..."); | |
return "update"; | |
} | |
} |
- 测试
登录访问 /test/update,没有权限访问,页面跳转 403 页面,控制台打印 update...
# @PostFilter
@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据
表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
- 修改 TestController
@RestController | |
@RequestMapping("/test") | |
public class TestController { | |
... | |
@RequestMapping("getAll") | |
@PreAuthorize("hasAnyAuthority('admin')") | |
@PostFilter("filterObject.username == 'admin1'") | |
public List<Users> getAllUser(){ | |
ArrayList<Users> list = new ArrayList<>(); | |
list.add(new Users(1,"admin1","6666")); | |
list.add(new Users(2,"admin2","888")); | |
return list; | |
} | |
} |
- 修改 User 类
@Data | |
@NoArgsConstructor | |
@AllArgsConstructor | |
public class Users { | |
private Integer id; | |
private String username; | |
private String password; | |
} |
- 测试
登录成功之后访问 /test/getAll
页面只出现 [{"id":1,"username":"admin1","password":"6666"}] 一条数据
# @PreFilter
@PreFilter: 进入控制器之前对数据进行过滤
# 用户注销
- 新增 success.html
登录成功 | |
<a href="/logout">退出</a> |
- 修改配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
/*@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); | |
String password = passwordEncoder.encode("123456"); | |
auth.inMemoryAuthentication().withUser("zhangsan").password(password).roles("admin"); | |
}*/ | |
@Autowired | |
private UserDetailsService userDetailsService; | |
@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
auth.userDetailsService(userDetailsService).passwordEncoder(password()); | |
} | |
@Bean | |
PasswordEncoder password(){ | |
return new BCryptPasswordEncoder(); | |
} | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
// 退出 | |
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll(); | |
// 配置没有权限访问跳转自定义页面 | |
http.exceptionHandling().accessDeniedPage("/unauth.html"); | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/success.html").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasRole("sale") | |
.anyRequest().authenticated() | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
- 测试
访问 success.html,跳回登录页面,进行登录
登录成功之后新开一个页面访问 /test/index,出现 Hello World
退出登录,再次访问 /test/index,跳回登录页面
# 基于数据库记住我
# 创建表
CREATE TABLE `persistent_logins` ( | |
`username` varchar(64) NOT NULL, | |
`series` varchar(64) NOT NULL, | |
`token` varchar(64) NOT NULL, | |
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE | |
CURRENT_TIMESTAMP, | |
PRIMARY KEY (`series`) | |
) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
# 编写配置类
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Autowired | |
private UserDetailsService userDetailsService; | |
@Autowired | |
private DataSource dataSource; | |
// 配置对象 | |
@Bean | |
public PersistentTokenRepository persistentTokenRepository(){ | |
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); | |
// 赋值数据源 | |
jdbcTokenRepository.setDataSource(dataSource); | |
// 自动创建表,第一次执行会创建,以后要执行就要删除掉! | |
//jdbcTokenRepository.setCreateTableOnStartup(true); | |
return jdbcTokenRepository; | |
} | |
@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
auth.userDetailsService(userDetailsService).passwordEncoder(password()); | |
} | |
@Bean | |
PasswordEncoder password(){ | |
return new BCryptPasswordEncoder(); | |
} | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
// 退出 | |
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll(); | |
// 配置没有权限访问跳转自定义页面 | |
http.exceptionHandling().accessDeniedPage("/unauth.html"); | |
http.formLogin() // 自定义编写的登录页面 | |
.loginPage("/login.html") // 登录页面的设置 | |
.loginProcessingUrl("/user/login") // 登录访问路径 | |
.defaultSuccessUrl("/success.html").permitAll() // 登录成功之后,跳转路径 | |
.and().authorizeRequests() | |
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证 | |
.antMatchers("/test/index").hasRole("sale") | |
.anyRequest().authenticated() | |
.and().rememberMe().tokenRepository(persistentTokenRepository()) | |
.tokenValiditySeconds(60) // 设置有效时长,单位秒 | |
.userDetailsService(userDetailsService) | |
.and().csrf().disable(); // 关闭 crsf 防护 | |
} | |
} |
# 修改 login.html
<form action="/user/login" method="post"> | |
账号:<input type="text" name="username"> | |
<br> | |
密码:<input type="text" name="password"> | |
<br> | |
<input type="checkbox" name="remember-me">自动登录 | |
<br> | |
<input type="submit" value="登录"> | |
</form> |
name 属性值必须位 remember-me. 不能改为其他值
# 测试
选择自动登录,登录成功之后,访问 /test/index,依然可以使用!
数据库中新增了一条数据
# SpringSecurity 微服务权限方案
# 什么是微服务
# 微服务由来
微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种 使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量 级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制 来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限 度的集中式管理。
# 微服务优势
(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比 较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql 等),数据库也是单个模块对应自己的数据库。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
# 微服务本质
(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构 使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务 之间在结构上 “松耦合”,而在功能上则表现为一个统一的整体。这种所谓的 “统一的整 体” 表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过 程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署
# 微服务认证与授权实现思路
# 认证授权过程分析
(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找 到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限 信息中去
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式 进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限 值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息 生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带 到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前 用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前 请求是否有权限访问
未完待续...