SpringSecurity+JWT
一、Spring Security简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security致力于为Java应用程序提供身份验证和授权的能力。像所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足定制需求的能力。
Spring Security两大重要核心功能:用户认证(Authentication)和用户授权(Authorization)。
- 用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
- 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
二、快速开始
使用Springboot工程搭建Spring Security项目。
1.引入依赖
在pom中新增了Spring Security的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
2.创建测试访问接口
用于访问接口时触发Spring Security登陆页面
package com.qf.my.ss.demo.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** web controller* @author Thor* @公众号 Java架构栈*/
@RestController
public class SecurityController {@RequestMapping("/hello")public String hello(){return "hello security";}
}
3.访问接口,自动跳转至Security登陆页面
访问add接口,讲自动跳转至Security的登陆页面
默认账号是: user
默认密码是:启动项目的控制台中输出的密码
三、Spring Security基础概念
在上一节中访问add接口,发现被Spring Security的登陆页面拦截,可以猜到这是触发了Security框架的过滤器。Spring Security本质上就是一个过滤器链。下面讲介绍Security框架的过滤器链。
1.过滤器链
- WebAsyncManagerIntegrationFilter:将SecurityContext集成到Spring MVC中用于管理异步请求处理的WebAsyncManager中。
- SecurityContextPersistenceFilter:在当前会话中填充SecurityContext,SecurityContext即Security的上下文对象,里面包含了当前用户的认证及权限信息等。
- HeaderWriterFilter:向请求的Header中添加信息
- CsrfFilter:用于防止CSRF(跨域请求伪造)攻击。Spring Security会对所有post请求验证是否包含系统生成的CSRF的信息,如果不包含则报错。
- LogoutFilter:匹配URL为“/logout”的请求,清除认证信息,实现用户注销功能。
- UsernamePasswordAuthenticationFilter:认证操作的过滤器,用于匹配URL为“/login”的POST请求做拦截,校验表单中的用户名和密码。
- DefaultLoginPageGeneratingFilter:如果没有配置登陆页面,则生成默认的认证页面
- DefaultLogoutPageGeneratingFilter:用于生成默认的退出页面
- BasicAuthenticationFilter:用于Http基本认证,自动解析Http请求头中名为Authentication的内容,并获得内容中“basic”开头之后的信息。
- RequestCacheAwareFilter:用于缓存HttpServletRequest
- SecurityContextHolderAwareRequestFilter:用于封装ServletRequest,让ServletRequest具备更多功能。
- AnonymousAuthenticationFilter:对于未登录情况下的处理,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中
- SessionManagementFilter:限制同一用户开启多个会话
- ExceptionTranslationFilter:异常过滤器,用来处理在认证授权过程中抛出异常。
- FilterSecurityInterceptor:获取授权信息,根据SecurityContextHolder中存储的用户信息判断用户是否有权限访问
2.过滤器加载过程
Springboot在整合Spring Security项目时会自动配置DelegatingFilterProxy过滤器,若非Springboot工程,则需要手动配置该过滤器。
过滤器如何进行加载的?
结合上图和源码,Security在DelegatingFilterProxy的doFilter()调用了initDelegat()方法,在该方法中调用了WebApplicationContext的getBean()方法,该方法出发FilterChainProxy的doFilterInternal方法,用于获取过滤链中的所有过滤器并进行加载。
四、Spring Security的认证方式-基本认证
1.认证概念
所谓的认证,就是用来判断系统中是否存在某用户,并判断该用户的身份是否合法的过程,解决的其实是用户登录的问题。认证的存在,是为了保护系统中的隐私数据与资源,只有合法的用户才可以访问系统中的资源。
2.认证方式
在Spring Security中,常见的认证方式可以分为HTTP层面和表单层面,常见的认证方式如下:
HTTP基本认证
Form表单认证
HTTP摘要认证
3.基本认证
HTTP基本认证是在RFC2616标准中定义的一种认证模式,它以一种很简单的方式与用户进行交互。HTTP基本认证可以分为如下4个步骤:
客户端首先发起一个未携带认证信息的请求;
然后服务器端返回一个401 Unauthorized的响应信息,并在WWW-Authentication头部中说明认证形式:当进行HTTP基本认证时,WWW-Authentication会被设置为Basic realm=“被保护的页面”;
接下来客户端会收到这个401 Unauthorized响应信息,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码使用冒号进行拼接并用Base64编码,然后将其放入到请求的Authorization头部并发送给服务器;
最后服务器端对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容。
HTTP基本认证是一种无状态的认证方式,与表单认证相比,HTTP基本认证是一种基于HTTP层面的认证方式,无法携带Session信息,也就无法实现Remember-Me功能。另外,用户名和密码在传递时仅做了一次简单的Base64编码,几乎等同于以明文传输,极易被进行密码窃听和重放攻击。所以在实际开发中,很少会使用这种认证方式来进行安全校验。
基本认证的代码实现:
- 创建SecurityConfig配置类
package com.qf.my.ss.demo.config;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;/*** @author Thor* @公众号 Java架构栈*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {//1.配置基本认证方式http.authorizeRequests().anyRequest().authenticated().and()//开启basic认证.httpBasic();}
}
- Basic认证详解
在未登录状态下访问目标资源时,查看响应头,可以看到WWW-Authenticate认证信息:WWW-Authenticate:Basic realm=“Realm”,其中WWW-Authenticate: 表示服务器告知浏览器进行代理认证工作。Basic: 表示认证类型为Basic认证。realm=“Realm”: 表示认证域名为Realm域。
根据401和以上响应头信息,浏览器会弹出一个对话框,要求输入 用户名/密码,Basic认证会将其拼接成 “用户名:密码” 格式,中间是一个冒号,并利用Base64编码成加密字符串xxx;然后在请求头中附加 Authorization: Basic xxx 信息,发送给后台认证;后台需要利用Base64来进行解码xxx,得到用户名和密码,再校验 用户名:密码 信息。如果认证错误,浏览器会保持弹框;如果认证成功,浏览器会缓存有效的Base64编码,在之后的请求中,浏览器都会在请求头中添加该有效编码。
五、Form表单认证
在SpringBoot开发环境中,只要我们添加了Spring Security的依赖包,就会自动实现表单认证。可以通过WebSecurityConfigurerAdapter提供的configure方法看到默认的认证方式就是表单认证
protected void configure(HttpSecurity http) throws Exception {logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();}
1.表单认证中的预置url和页面
默认的formLogin配置中,自动配置了一些url和页面:
- /login(get): get请求时会跳转到这个页面,只要我们访问任意一个需要认证的请求时,都会跳转到这个登录界面。
- /login(post): post请求时会触发这个接口,在登录页面点击登录时,默认的登录页面表单中的action就是关联这个login接口。
- /login?error: 当用户名或密码错误时,会跳转到该页面。
- /: 登录成功后,默认跳转到该页面,如果配置了index.html页面,则 ”/“ 会重定向到index.html页面,当然这个页面要由我们自己实现。
- /logout: 注销页面。
- /login?logout: 注销成功后跳转到的页面。
由此可见,SpringSecurity默认有两个login,即登录页面和登录接口的地址都是 /login:
- GET http://localhost:8080/login
- POST http://localhost:8080/login
如果是 GET 请求,表示你想访问登录页面;如果是 POST 请求,表示你想提交登录数据。
对于这几个URL接口,我们简单了解即可。
2.自定义认证页面
- 自定义登陆页面
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --><title>Bootstrap 101 Template</title><!-- Bootstrap --><link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"><!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 --><!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 --><!--[if lt IE 9]><script src="https://cdn.jsdelivr.cn/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script><script src="https://cdn.jsdelivr.cn/npm/respond.js@1.4.2/dest/respond.min.js"></script><![endif]--><style>.login-top{width: 600px;height: 300px;border: 1px solid #DCDFE6;margin: 150px auto;padding: 20px 50px 20px 30px;border-radius: 20px;box-shadow: 0px 0px 20px #DCDFE6;}</style>
</head>
<body>
<div class="login-top"><div><h3>欢迎登陆</h3></div><form action="/login" method="post"><div class="form-group" style="padding-bottom: 20px"><label for="inputUsername" class="col-sm-2 control-label">用户名</label><div class="col-sm-10"><input type="text" class="form-control" id="inputUsername" name="username" placeholder="用户名"></div></div><div class="form-group"><label for="inputPassword" class="col-sm-2 control-label">密码</label><div class="col-sm-10"><input type="password" class="form-control" id="inputPassword" name="password" placeholder="密码"></div></div><div class="form-group"><div class="col-sm-offset-2 col-sm-10"><div class="checkbox"><label><input type="checkbox"> 记住我</label></div></div></div><div class="form-group"><div class="col-sm-offset-2 col-sm-10"><button type="submit" class="btn btn-default">登陆</button></div></div></form></div>
</body>
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script><!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.2/css/bootstrap-utilities.min.css" rel="stylesheet"></body>
</html>
-
自定义首页
-
自定义错误页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><h3>用户名或密码错误</h3>
</body>
</html>
3.自定义配置项
package com.qf.my.ss.demo.config;import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/*** @author Thor* @公众号 Java架构栈*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/js/**","/css/**","/images/**");}@Overrideprotected void configure(HttpSecurity http) throws Exception {//1.配置基本认证方式http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll()
//指登录成功后,是否始终跳转到登录成功url。它默认为false.defaultSuccessUrl("/index.html",true)
//post登录接口,登录验证由系统实现.loginProcessingUrl("/login")
//用户密码错误跳转接口.failureUrl("/error.html")//要认证的用户参数名,默认username.usernameParameter("username")
//要认证的密码参数名,默认password.passwordParameter("password").and()
//配置注销.logout()
//注销接口.logoutUrl("/logout")
//注销成功后跳转到的接口.logoutSuccessUrl("/login.html").permitAll()
//删除自定义的cookie.deleteCookies("myCookie").and()
//注意:需禁用crsf防护功能,否则登录不成功.csrf().disable();}
}
4.WebSecurity和HttpSecurity
Spring Security内部是如何加载我们自定义的登录页面的?需要了解这两个类:WebSecurity和HttpSecurity。
- WebSecurity
在这个类里定义了一个securityFilterChainBuilders集合,可以同时管理多个SecurityFilterChain过滤器链,
当WebSecurity在执行时,会构建出一个名为 ”springSecurityFilterChain“ 的 Spring BeanFilterChainProxy代理类,它的作用是来 定义哪些请求可以忽略安全控制,哪些请求必须接受安全控制;以及在合适的时候 清除SecurityContext 以避免内存泄漏,同时也可以用来 定义请求防火墙和请求拒绝处理器,也可以在这里 开启Spring Security 的Debug模式。
- HttpSecurity
HttpSecurity用来构建包含一系列的过滤器链SecurityFilterChain,平常我们的配置就是围绕着这个SecurityFilterChain进行。
5.Http摘要认证
- 概念
HTTP摘要认证和HTTP基本认证一样,也是在RFC2616中定义的一种认证方式,它的出现是为了弥补HTTP基本认证存在的安全隐患,但该认证方式也并不是很安全。HTTP摘要认证会使用对通信双方来说都可知的口令进行校验,且最终以密文的形式来传输数据,所以相对于基本认证来说,稍微安全了一些。
**HTTP摘要认证与基本认证类似,基于简单的“挑战-回应”模型。**当我们发起一个未经认证的请求时,服务器会返回一个401回应,并给客户端返回与验证相关的参数,期待客户端依据这些参数继续做出回应,从而完成整个验证过程。
- 摘要认证核心参数
服务端给客户端返回的验证相关参数如下:
username: 用户名。password: 用户密码。realm: 认证域,由服务器返回。opaque: 透传字符串,客户端应原样返回。method: 请求的方法。nonce: 由服务器生成的随机字符串,包含过期时间(默认过期时间300s)和密钥。nc: 即nonce-count,指请求的次数,用于计数,防止重放攻击。qop被指定时,nc也必须被指定。cnonce: 客户端发给服务器的随机字符串,qop被指定时,cnonce也必须被指定。qop: 保护级别,客户端根据此参数指定摘要算法。若取值为 auth,则只进行身份验证;若取值为auth-int,则还需要校验内容完整性,默认的qop为auth。uri: 请求的uri。response: 客户端根据算法算出的摘要值,这个算法取决于qop。algorithm: 摘要算法,目前仅支持MD5。entity-body: 页面实体,非消息实体,仅在auth-int中支持。
通常服务器端返回的数据包括realm、opaque、nonce、qop等字段,如果客户端需要做出验证回应,就必须按照一定的算法得到一些新的数据并一起返回。在以上各种参数中,对服务器而言,最重要的字段是nonce;对客户端而言,最重要的字段是response。
- 摘要认证的实现
package com.qf.my.spring.security.demo.config;import com.qf.my.spring.security.demo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
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.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;/*** 摘要认证的配置* @author Thor* @公众号 Java架构栈*/
@EnableWebSecurity
public class DigestConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate DigestAuthenticationEntryPoint digestAuthenticationEntryPoint;@Autowiredprivate MyUserDetailService userDetailService;//配置认证入口端点,主要是设置认证参数信息@Beanpublic DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();point.setKey("security demo");point.setRealmName("thor");point.setNonceValiditySeconds(500);return point;}public DigestAuthenticationFilter digestAuthenticationFilter(){DigestAuthenticationFilter filter = new DigestAuthenticationFilter();filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint);filter.setUserDetailsService(userDetailService);return filter;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/hello").hasAuthority("role").anyRequest().authenticated().and().csrf(