一. BCrypt密码加密
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样。
1.1 准备工作
(1)tensquare_user工程的pom引入SpringSecurity依赖
(2)添加配置类 (资源/工具类中提供)
因为使用SpringBoot是帮我将SpringSecurity默认配置好了的,但其默认配置是将所有路径进行拦截,所以我们要重新写个配置文件来替换它默认的配置文件,但spingboot中没有地方写配置文件,所以我们使用配置类。(用的纯注解开发一定是springboot,不是springboot也可以用纯注解开发,但比写配置文件麻烦,所以一般spring开发遵循:自己写的类用注解,框架类用配置文件。spring开发的入口是xml文件,springboot强就强在把xml拿掉了)
package com.tensquare.user.config;
import org.springframework.context.annotation.Configuration;
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;
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
//authorizeRequests所有security全注解配置实现的开端,表示开始说明需要的权限。
//需要的权限分两部分,第一部分是拦截的路径,第二部分访问该路径需要的权限。
//antMatchers表示拦截什么路径,permitAll任何权限都可以访问,直接放行所有。
//anyRequest() 任何请求,authenticated认证后才能访问
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
1.2 加密添加admin用户
要加密,我们一定要用到加密那个类,所以我们要把那个类放到容器中:
controller层和service层代码如下:
然后我们测试下就可以看到确实密码加密了
二 常见的认证机制
2.1 HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth
2.2 Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删
除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;
2.3 OAuth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。下面是OAuth2.0的流程图
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。
2.4 Token Auth
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
Token Auth的优点,Token机制相对于Cookie机制又有什么好处呢?
- 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
- 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
- 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
- CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
- 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
三 基于JWT的Token认证机制实现
3.1 什么是JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
3.2 JWT的组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行base64编码,得到Jwt的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
四 Java的JJWT实现JWT
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
下面是JJWT的快速入门
首先导入jar包:
4.1 JJWT生成令牌
package com.tensquare.jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class CreateJwt {
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("666") //id
.setSubject("小马") //名称
.setIssuedAt(new Date()) //时间
.signWith(SignatureAlgorithm.HS256 , "itcast") //盐
.setExpiration(new Date(new Date().getTime() + 60000)) //设置过期时间
.claim("role" , "admin"); //添加自定义属性 角色
System.out.println(jwtBuilder.compact()); //输出令牌
}
}
运行出来会得到下面的令牌:
4.2 token解析
package com.tensquare.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.text.SimpleDateFormat;
public class ParseJwtTest {
public static void main(String[] args) {
Claims claims = Jwts.parser()
.setSigningKey("itcast")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_pqawiLCJpYXQiOjE1NTM5MTA5MzgsImV4cCI6MTU1MzkxMDk5OCwicm9sZSI6ImFkbWluIn0.TSRDkK_bVqyeKWL3iaDDJugtZ-1F3QmPN0R1VMURGPw")
.getBody();
System.out.println("用户id:"+claims.getId());
System.out.println("用户名:"+claims.getSubject());
System.out.println(String.format("登录时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt())));
System.out.println("过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getExpiration()));
}
}