文章目录
前言
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。
如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:
- 一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;
- 另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。
本文以记录学习 shiro 为主,其它内容可能很潦草
我们先了解几个概念,再介绍几种解决方案
概念
无状态登录
什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端 session 中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
什么是无状态?
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
带来的好处是什么呢?
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩
- 减小服务端存储压力
如何实现无状态?
无状态登录的流程:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成 token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的 token
- 服务的对 token 进行解密,判断是否有效。
常见的认证机制
- HTTP Basic Auth,是配合 RESTful API 使用的最简单的认证方式,只需提供用户名密码即可
- Cookie Auth,通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的
- OAuth,(开放授权)是一个开放的授权标准。
- OAuth允许用户提供一个令牌,让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
- 适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。
- Token Auth,用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录
JWT 鉴权
什么是JWT ?
Json web token,JWT 是目前最流行的跨域认证解决方案。基于json的开放标准(RFC7用于519),以 token的方式代替传统的 session-cookie 模式,可实现无状态、分布式的Web应用授权。用于服务器,客户端传递信息签名验证。
JWT包含三部分数据:
-
Header
:头部,通常头部有两部分信息:- 声明类型,这里是 JWT
我们会对头部进行base64编码,得到第一部分数据
-
Payload
:载荷,就是有效数据,一般包含下面信息:- 用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
- 注册声明:如token的签发时间,过期时间,签发人等
这部分也会采用base64编码,得到第二部分数据
-
Signature
:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
生成的数据格式:token==个人证件 jwt=个人身份证
传统的cookie-session鉴权:
-
客户端使用用户名和密码登录
-
服务器端验证账号密码通过后,在 session 里保存一些数据(比如说用户UID,登录时间等等)
-
服务器向用户返回一个 session_id,写入用户的cookie中
-
此后用户的每一次请求都用 把 cookie 中的这个 session_id 传给服务器
-
服务器接收到 session_id 找到之前保存的数据就可以知道用户有没有登录
传统方式的缺点:
- session通常放在内存中,用户数量如果过大会对服务器产生压力
- 扩展性。 哪怕session以文件形式保存,放在redis中。对于分布式系统来说会产生高流量的数据读取(文件同步读取问题)
- 容易受到 csrf 攻击
JWT 的验证方式
- 用户登录
- 服务的认证,通过后根据 secret 生成token
- 将生成的 token 返回给浏览器
- 用户每次请求携带 token
- 服务端利用公钥解读 jwt 签名,判断签名有效后,从 Payload 中获取用户信息
- 处理请求,返回响应结果
因为 JWT 签发的 token 中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了 Rest 的无状态规范。
JWT的优点:
-
服务器不用 session了,变为无状态。减小了开支
-
jwt 构成简单,占用很少的字节
-
json 格式通用。不用语言之间都可以处理
非对称加密
加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
- 对称加密,如 AES
- 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
- 优势:算法公开、计算量小、加密速度快、加密效率高
- 缺陷:双方都使用同样密钥,安全性得不到保证
- 非对称加密,如 RSA
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 优点:安全,难以破解
- 缺点:算法比较耗时
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 不可逆加密,如 MD5,SHA
- 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。
Spring中的拦截器
Spring为我们提供了org.springframework.web.servlet.handler.HandlerInterceptorAdapter
这个适配器,继承此类,可以非常方便的实现自己的拦截器。
他有三个方法:
- 预处理 preHandle
- 后处理(调用了Service并返回 ModelAndView,但未进行页面渲染)、
- 返回处理(已经渲染了页面)
示意实现 JWT配置类写法:
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1.通过request获取请求token信息
String authorization = request.getHeader("Authorization");
//判断请求头信息是否为空,或者是否已Bearer开头
if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")) {
//获取token数据
String token = authorization.replace("Bearer ","");
//解析token获取claims
Claims claims = jwtUtils.parseJwt(token);
if(claims != null) {
//通过claims获取到当前用户的可访问API权限字符串
String apis = (String) claims.get("apis"); //api-user-delete,api-userupdate
//通过handler
HandlerMethod h = (HandlerMethod) handler;
//获取接口上的reqeustmapping注解
RequestMapping annotation =
h.getMethodAnnotation(RequestMapping.class);
//获取当前请求接口中的name属性
String name = annotation.name();
//判断当前用户是否具有响应的请求权限
if(apis.contains(name)) {
request.setAttribute("user_claims",claims);
return true;
}else {
throw new CommonException(ResultCode.UNAUTHORISE);
}
}
}
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
}
SpringSecurity
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。对于安全控制,我们仅需要引入 spring-boot-starter-security
模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter
:自定义Security策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启WebSecurity模式
Spring Security 的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
这个概念是通用的,而不是只在Spring Security 中存在。
简单示例
这里介绍基本的登录登出认证操作,供入门了解
建议通过阅读源码练习,进入 对应的重写方法参数对象查看
参考官网:https://spring.io/projects/spring-security
参考官网:https://spring.io/projects/spring-security
- 引入 Spring Security 模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 非常简易的写个 Controller
@Controller
public class RouterController {
//首页
@RequestMapping({
"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String tologin(){
return "login";
}
@RequestMapping("/level1/{id}")
public String tologin(@PathVariable("id") int id){
return "views/level1"+id;
}
//...
}
-
编写 Spring Security 配置类
继承 WebSecurityConfigurerAdapter 类,重写 configure 方法
稍微提一下
该框架有一个很大的特点就是:链式编程
使用 HttpSecurity
对象,编写授权方法
-
http.formLogin();
开启登录(该框架自动提供了登录页面,也可自己定义).loginPage("/toLogin")
,自己定义登录页面
-
http.logout()
,开启自动配置的注销 -
http.rememberMe()
,开启"记住我"
功能.rememberMeParameter("remember")
,
使用 AuthenticationManagerBuilder
对象 编写认证方法
- 在内存中定义,也可以在 jdbc 中去拿(示例为从内存中拿)
- Spring security 5.0中新增了多种加密方式,也改变了密码的格式。官方推荐的是使用 bcrypt 加密方式。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin()
.usernameParameter("username")//配置接收登录的用户名和密码的参数!
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
//开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"); 注销成功来到首页
// sample logout customization,这里也可以选择,清空cookie 与 session
//http.logout().deleteCookies("remove").invalidateHttpSession(false)
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
//记住我
//自定义接收前端参数!
http.rememberMe().rememberMeParameter("remember");
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
}
上面代码示例了从内存中获取认证,下面截取使用数据库方式的官方文档参考:
import javax.sql.DataSource;
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,enabled from users WHERE username=?")
.authoritiesByUsernameQuery("select username,authority from authorities where username=?")
.passwordEncoder(new BCryptPasswordEncoder());
}
记住我功能如何实现的呢?其实非常简单
登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个 cookie
我们可以查看浏览器的 cookie