内容
3.1基本概念
3.1.1 什么是认证
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
认证:用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
3.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 的方式更适合。
3.1.3 什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
3.1.4 授权的数据模型
如何进行授权即如何对用户访问资源进行控制,首先需要学习授权相关的数据模型。
授权可简单理解为Who对What(which)进行How操作,包括如下:
Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。 What,即资源(Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息都属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号 为001的商品为资源实例。How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。
主体、资源、权限相关的数据模型如下:
主体(用户id、账号、密码、…)
权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)
角色(角色id、角色名称、…)
角色和权限关系(角色 id、权限id、…)
主体(用户)和角色关系(用户id、角色id、…)
3.1.5 RBAC
如何实现授权?业界通常基于RBAC实现授权。
3.1.5.1 基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:
案例:
if(主体.hasRole("总经理角色id")){
查询工资
}
问题:如果查询工资的角色变化为总经理和部门经理,还有项目经理可访问,此时就需要修改判断逻辑
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")
|| 主体.hasRole("项目经理角色id")){
查询工资
}
根据上边案例发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可拓展性差
3.1.5.2 基于资源的访问控制
按资源进行授权(Resource-Based Access Controller)
aaa
代码案例:
if(主体.hasPermission("查询公司权限标识")){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化,都不需要修改授权代码,系统可扩展性强
3.2. 基于 Session 的认证方式
3.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失效 |
3.3 SpringSecurity
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
SpringSecurity的核心功能:
用户认证(Authentication):系统判断用户是否能登录
用户授权(Authorization):系统判断用户是否有权限去做某些事情
SpringSecurity 特点:
Spring 技术栈的组成部分,与Spring 无缝整合。
全面的权限控制,能提供完整可扩展的认证和授权支持保护
专门为 Web 开发而设计。
重量级,需要引入各种家族组件与依赖
3.4 SpringSecurity 快速入门
1.创建springboot项目
对应的pom文件中的核心jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
运行项目显示的页面
该页面在登录的时候默认的用户名是user密码显示在控制台
输入用户名和密码后即可访问成功。
(课堂演示即可)
权限管理中的相关概念:
名称 | 英文名称 | 概念 |
主体 | principal | 使用系统的用户或设备或从其他系统远程登录的用户等等 |
认证 | authentication | 权限管理系统(通过登录操作)确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。 |
授权 | authorization | 给用户分配权限:将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。 |
3.5 SpringSecurity 基本原理
SpringSecurity 本质是一个过滤器链,由许多过滤器组成,重点看一下几个过滤器:
(ctrl+N进行全局搜索)
ExceptionTranslationFilter: 异常过滤器,用来处理在认证授权过程中抛出的异常
表单登录
Servlet Authentication Architecture :: Spring Security
提交用户名和密码后,将对用户名和密码进行身份验证。 扩展了 AbstractAuthenticationProcessingFilter,因此下图应该看起来非常相似:
该图基于我们的 SecurityFilterChain 图。
- 当用户提交其用户名和密码时,通过从实例中提取用户名和密码来创建一种身份验证类型。UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationToken HttpServletRequest
- 接下来,将 传递到要进行身份验证的实例中。 外观的详细信息取决于用户信息的存储方式。UsernamePasswordAuthenticationToken AuthenticationManagerAuthenticationManager
- 如果身份验证失败,则失败。
SecurityContextHolder 被清除。
RememberMeServices.loginFail被调用。 如果未配置“记住我”,则这是无操作。 请参阅 Javadoc 中的 RememberMeServices 接口。
AuthenticationFailureHandler被调用。 请参阅 Javadoc 中的 AuthenticationFailureHandler 类
- 如果身份验证成功,则成功。
SessionAuthenticationStrategy收到新登录的通知。 请参阅 Javadoc 中的 SessionAuthenticationStrategy 接口。
身份验证在 SecurityContextHolder 上设置。 请参阅 Javadoc 中的 SecurityContextPersistenceFilter 类。
RememberMeServices.loginSuccess被调用。 如果未配置“记住我”,则这是无操作。 请参阅 Javadoc 中的 RememberMeServices 接口。
ApplicationEventPublisher发布 .InteractiveAuthenticationSuccessEvent
被调用。通常,这是一个 ,当我们重定向到登录页面时,它会重定向到 ExceptionTranslationFilter 保存的请求。AuthenticationSuccessHandlerSimpleUrlAuthenticationSuccessHandler
UsernamePasswordAuthenticationFilter : 对/login 的 POST 请求做拦截,校验表单中用户名,密码。
发出post
name=username
name=password
3.6 自定义用户名密码
方式一:通过配置文件设置
spring.security.user.name=yyl
spring.security.user.password=yyl
方式二:通过配置类进行配置
Spring Security without the WebSecurityConfigurerAdapter
内存身份校验
In-Memory Authentication :: Spring Security
package com.example.securitydemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 创建密码解析器
BCryptPasswordEncoder pe =new BCryptPasswordEncoder();
// 对密码进行加密
String password = pe.encode("123456");
auth.inMemoryAuthentication()
.passwordEncoder(pe) //默认没有,需要手动设置BCryptPasswordEncoder
.withUser("yyl")
.password(password).roles("admin")
;
}
}
PasswordEncoder 接口:
把参数按照特定的解析规则进行解析:String encode(CharSequence rawPassword);
验证从存储中获取的编码密码与编码后提交的原始密码是否匹配:boolean matches(CharSequence rawPassword, String encodedPassword); //raw:需要被解析的密码。encode:存储的密码。
判断解析的密码能否再次进行解析且达到更安全的结果:default boolean upgradeEncoding(String encodedPassword) {return false; }
接口实现类BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,是对 bcrypt 强散列方法的具体实现。平时多使用这个解析器。(BCryptPasswordEncoder基于 Hash 算法实现单向加密,可以通过 strength 控制加密强度,默认 10.)
方式三:自定义实现类完成用户登录
表单登录
UserDetailsService 接口讲解:
而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现UserDetailsService 接口即可。
User是 UserDetails 实现类,我们也可以使用 User 这个实体类:
编写实现类,实现UserDetailsService接口:
package com.example.securitydemo.service;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserService implements UserDetailsService {
/**
* 根据用户名 返回用户的信息
* @param s
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 获取输入的用户名 s
System.out.println("输入的用户名是============:"+s);
List<GrantedAuthority> roles = new ArrayList<>();
roles.add(new SimpleGrantedAuthority("admin"));
User user = new User("yyl",new BCryptPasswordEncoder().encode("123456"),roles);
return user;
}
}
修改配置类:
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 需要注入 userDetailService
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
auth.userDetailsService(userDetailsService).passwordEncoder(getPassword());
}
// 注入password的bean
public PasswordEncoder getPassword(){
return new BCryptPasswordEncoder();
}
}
3.7 自定义页面与权限控制
自定义登陆页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/userlogin" method="post">
用户名:<input type="text" name="myname">
<br/>
用户名:<input type="password" name="mypwd">
<br/>
<input type="submit" value="login">
</form>
</body>
</html>
修改配置类:
SecurityConfig 添加configure方法
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开始 配置自定义的信息
http.formLogin().loginPage("/login.html")
.usernameParameter("myname") // 设置接收的自定义的用户的参数
.passwordParameter("mypwd") // 设置接收自定义的用户的密码
.loginProcessingUrl("/userlogin") // 登录的路径 跟html或者jsp中的登录路径保持一致即可
.defaultSuccessUrl("/success") // 默认的成功的路径
.and().authorizeRequests().antMatchers("/","/userlogin").permitAll()
.and().authorizeRequests().antMatchers("/test/**").hasAuthority("user") // 有user的权限
.anyRequest().authenticated() // 除去不需要认证的路径的其它路径都需要认证
//.and().exceptionHandling().accessDeniedPage("/unauth.html") // 自定义没有权限的页面
.and()
.csrf().disable();// 关闭csrf的保护
}
相关方法: 角色和权限都可以设置多个,以逗号分开
方法名称 | 说明 |
hasAuthority | 如果当前的主体具有指定的权限,则可以访问 |
hasAnyAuthority | 如果当前的主体有任何提供的角色的话,就可以访问 |
hasRole | 如果用户具备给定角色就允许访问 |
hasAnyRole | 用户具备任何一个角色都可以访问 |
当User对象没有对应权限时会返回403错误
可以自定义403页面
页面内容 自行编写
配置文件中的需要添加
http.exceptionHandling().accessDeniedPage("/403.html")
3.8 注解的使用
使用注解前需要在启动器或配置类上添加注解:
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true,jsr250Enabled=true)
@Secured:判断是否具有角色:
@Secured({"ROLE_admin"})
@RequestMapping("testSecured")
public String testSecured() {
return "testSecured";
}
注意:使用这个这个注解的时候对应的角色信息必须是以ROLE_**的形式出现,否则无法识别
运行结果:
@RolesAllowed:判断是否具有角色:
@RolesAllowed({"admin"})
@RequestMapping("testSecured")
public String testSecured() {
return "testSecured";
}
注意:此注解不需要特别添加前缀ROLE_
@PreAuthorize:进入方法前进行权限验证, @PreAuthorize 可以将登录用户的 权限参数传到方法中。
@RequestMapping("/preAuthorize")
//@PreAuthorize("hasRole('ROLE_admin')")
@PreAuthorize("hasAnyAuthority('findAll')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}
@PostAuthorize:方法执行后再进行权限验证,适合验证带有返回值的权限:
3.9 用户注销
1. 在配置类中添加退出映射地址:
http.logout().logoutUrl("/logout")
.logoutSuccessUrl("/login.html").permitAll()
2. 在登录页面添加一个退出连接:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>success</title>
</head>
<body>
登录成功<br> <a href="/logout">退出</a>
</body>
</html>
修改登陆成功后的跳转页面
3.10 跨站请求伪造
跨站请求伪造(英语:Cross-site request forgery,CSRF), 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。
这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
使用CSRF:
1.需要在pom文件中添加相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
2.在登录页面添加一个隐藏域,
使用了 Thymeleaf模板: 因为使用了Thymeleaf,我们不能像上面一样访问**.html
的静态页面,而是通过controller返回地址,页面才能解析。
@GetMapping( "/login")
public String testLogin() {
return "login";
}
3.在登陆页面添加一个隐藏域:
<input type="hidden" name="_csrf.parameterName" th:value="${_csrf.token}" th:if="${_csrf}!=null"/>
4.关闭安全配置的类中的 csrf
http.csrf().disable();
Spring Security 实现 CSRF 的原理:
1. 生成 csrfToken 保存到 HttpSession 或者 Cookie 中。
CsrfToken接口:
该接口有一个实现类:SaveOnAccessCsrfToken
2 请求到来时,从请求中提取 csrfToken
和保存的 csrfToken 做比较,进而判断当前请求是否合法。主要通过 CsrfFilter 过滤器来完成。