一、BCrypt密码加密
有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。Spring Security提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码,BCrypt强哈希方法每次加密的结果都不一样。
1.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.2 添加配置类
添加了Spring Security依赖后,所有的地址都被Spring Security所控制了,目前只需要用到BCrypt密码加密的部分,所以要添加一个配置类,配置为所有地址都可以匿名访问。
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//authorizeRequests所有security全注解配置实现的开端,表示开始说明需要的权限。
//需要的权限分两部分,第一部分是拦截的路径,第二部分访问该路径需要的权限。
//antMatchers表示拦截什么路径,permitAll表示任何权限都可以访问,直接放行所有。
//.anyRequest().authenticated() 任何的请求认证后才能访问
//.and().csrf().disable();固定写法,表示使csrf拦截失效。
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
1.3 在入口类配置Bean
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
1.4 密码加密与校验小案例
新增商家密码加密
@ApiOperation(value="添加商家信息")
@PostMapping
public Result insert(@ModelAttribute TbSeller seller){
seller.setId(new SnowFlake().nextId());
seller.setRegisterDate(new Date());
//密码加密
String newPassword = encoder.encode(seller.getLoginPassword());
seller.setLoginPassword(newPassword);
int insertNums = sellerService.insert(seller);
if(insertNums > 0){
return new Result(true,StatusCode.OK,"添加成功!");
}
return new Result(false,StatusCode.ERROR,"添加失败!");
}
商家登录密码校验
@ApiOperation(value="商家登录")
@PutMapping("/login")
public Result login(@ModelAttribute TbSeller seller){
String phone = seller.getPhone();
String email = seller.getEmail();
TbSeller sellerLogin = sellerService.selectByPhoneOrEmail(phone,email);//根据邮箱或手机号查询对象
if(sellerLogin != null && encoder.matches(seller.getLoginPassword(),sellerLogin.getLoginPassword())){
//数据库中的密码和用户输入的密码匹配相同则登录成功
return new Result(true, StatusCode.OK, "登录成功");
}
return new Result(false, StatusCode.LOGINERROR, "登录失败");
}
二、基于JWT的Token认证机制实现
2.1 什么是JWT
JSON Web Token(JWT)是为了网络应用环境间传递声明而执行的一种基于JSON的开发标准。token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。
2.2 基于Token的鉴权机制
基于Token的鉴权机制类似于http协议也是无状态的,不需要在服务端去存储用户的登录记录。
流程:
1.客户端使用用户名跟密码请求登录
2.服务端收到请求,验证用户名与密码
3.验证成功后,服务端会签发一个Token,再把这个Token发送给客户端
4.客户端存储Token,且每次向服务端请求资源时附加这个Token
5.服务端收到请求,验证客户端请求里面呆着的Token,如果验证成功就向客户端返回请求的数据
(这个token必须要在每次请求时保存在请求头中发送给服务器,另外,服务器要支持CORS(跨域资源共享)策略,一般在服务端Controller类贴上@CrossOrigin)
2.3 Token Auth的优点
1.支持跨域访问。
2.无状态:Token机制在服务端不需要存储session信息,因为Token自身包含了所有的登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
3.更适用CDN:可以通过内容分发网络请求服务端的所有资料(如:javascript,HTML,图片等),而服务端只要提供API即可。
4.去耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在API被调用的时候进行Token生成调用即可。
5.更适用于移动应用:当客户端是一个原生平台(iOS,Android,Windows 8等)时,cookic是不被支持的(需要通过cookie容器进行处理),这时采用Token认证机制就会简单的多。
6.CSRF:因为不再依赖于cookie,所以不需要考虑对CSRF(跨站请求伪造)的防范。
7.性能:一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多。
2.4 JWT组成
JWT由头部、载荷与签名三部分组成
2.4.1 头部 (header)
头部用于描述关于该JWT的最基本信息,例如其类型以及签名所用的算法等,这也可以被表示成一个JSON对象。
例如:
{“typ”:”JWT”,”alg”:“HS256”}
https://base64.supfree.net/ 进行base64编号后的字符串如下:
JTdCJXUyMDFDdHlwJXUyMDFEJTNBJXUyMDFESldUJXUyMDFEJTJDJXUyMDFEYWxnJXUyMDFEJTNBJXUyMDFDSFMyNTYldTIwMUQlN0Q=
2.4.2 载荷 (payload)
载荷就是存放有效信息的地方。这些有效信息包含三个部分:
1.标准中注册的声明 (建议但不强制使用)
iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
2.公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
3.私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
例如:
{“sub”:”user”,”name”:“lang”}
https://base64.supfree.net/ 进行base64编号得到JWT的第二部分:
JTdCJXUyMDFDc3ViJXUyMDFEJTNBJXUyMDFEdXNlciV1MjAxRCUyQyV1MjAxRG5hbWUldTIwMUQlM0EldTIwMUNsYW5nJXUyMDFEJTdE
2.4.3 签证 (signature)
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
header
payload
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。
o5YT-yGV8ug0Aa8UIQXoiA0rs1pKb2yfF8nECngiGPE
将这三部分用.连接成一个完整的字符串,构成了最终的JWT:
JTdCJXUyMDFDdHlwJXUyMDFEJTNBJXUyMDFESldUJXUyMDFEJTJDJXUyMDFEYWxnJXUyMDFEJTNBJXUyMDFDSFMyNTYldTIwMUQlN0Q=.JTdCJXUyMDFDc3ViJXUyMDFEJTNBJXUyMDFEdXNlciV1MjAxRCUyQyV1MjAxRG5hbWUldTIwMUQlM0EldTIwMUNsYW5nJXUyMDFEJTdE.o5YT-yGV8ug0Aa8UIQXoiA0rs1pKb2yfF8nECngiGPE
三、Java的JJWT实现JWT
3.1 什么是JJWT
JJWT是一个提供端到端的JWT创建和验证的Java库。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
3.2 JJWT快速入门
3.2.1 引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
3.2.2 生成Token测试
public class CreateJWT {
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("666")
.setSubject("浪臻")
.setIssuedAt(new Date())//设置签发时间
.signWith(SignatureAlgorithm.HS256,"lang")//设置签名秘钥
.setExpiration(new Date(new Date().getTime()+60000))//设置过期时间
.claim("role","seller");//添加自定义键值对
System.out.println(jwtBuilder.compact());
}
}
测试运行,输出如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLmtaroh7siLCJpYXQiOjE1NzQyMzk1OTIsImV4cCI6MTU3NDIzOTY1Miwicm9sZSI6InNlbGxlciJ9.RqjRh2hp9_NcWI_JldBZ9sxIdMkFAibS8edgzyZIh2o
再次运行,会发现每次运行的结果是不一样的,因为载荷中包含了时间
3.2.3 解析Token测试
public class ParseJWT {
public static void main(String[] args) {
Claims claims = Jwts.parser()
.setSigningKey("lang")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLmtaroh7siLCJpYXQiOjE1NzQyMzk1OTIsImV4cCI6MTU3NDIzOTY1Miwicm9sZSI6InNlbGxlciJ9.RqjRh2hp9_NcWI_JldBZ9sxIdMkFAibS8edgzyZIh2o")
.getBody();
System.out.println("用户id:"+claims.getId());
System.out.println("用户名:"+claims.getSubject());
System.out.println("登录时间:"+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()));
System.out.println("用户角色:"+claims.get("role"));
}
}
测试运行,输出如下:
用户id:666
用户名:浪臻
登录时间:2019-11-20 16:46:32
过期时间:2019-11-20 16:47:32
用户角色:seller
3.2.4 JWT工具类编写
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Date;
@ConfigurationProperties("jwt.config")
public class JWTUtil {
/**
* 盐
*/
private String key;
/**
* 过期时间
*/
private long deadline;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getDeadline() {
return deadline;
}
public void setDeadline(long deadline) {
this.deadline = deadline;
}
/**
* 生成JWT
* @param id
* @param subject
* @param roles
* @return
*/
public String createJWT(String id, String subject, String roles){
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key)
.claim("roles", roles);
if(deadline > 0){
builder.setExpiration(new Date(nowMillis + deadline));
}
return builder.compact();
}
/**
* 解析JWT
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
3.2.5 小案例
在入口类配置Bean
@Bean
public JWTUtil jwtUtil(){
return new JWTUtil();
}
application.yml配置
jwt:
config:
key: lang
# 设置过期时间为24小时
deadline: 86400000
拦截器负责把请求头中包含token的令牌进行解析验证
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import xyz.hclz.mobileshop.common.utils.JWTUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JWTUtil jwtUtil;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//拦截器只是负责把请求头中包含token的令牌进行解析验证
String header = request.getHeader("Authorization");
if(header != null && !"".equals(header)){
if(header.startsWith("Student ")){
String token = header.substring(8);//得到token
try{
Claims claims = jwtUtil.parseJWT(token);
String roles = (String) claims.get("roles");
if(roles != null || roles.equals("seller")){
request.setAttribute("claims_seller", token);
}
}catch (RuntimeException e){
throw new RuntimeException("Incorrect token!");
}
}
}
return true;
}
}
Controller
/**
* 商家登录
* @param seller
* @return
*/
@ApiOperation(value="商家登录")
@PutMapping("/login")
public Result login(@ModelAttribute TbSeller seller){
String phone = seller.getPhone();
String email = seller.getEmail();
TbSeller sellerLogin = sellerService.selectByPhoneOrEmail(phone,email);//根据邮箱或手机号查询对象
if(sellerLogin != null && encoder.matches(seller.getLoginPassword(),sellerLogin.getLoginPassword())){
//数据库中的密码和用户输入的密码匹配相同则登录成功
//生成令牌
String token = jwtUtil.createJWT(sellerLogin.getId(), sellerLogin.getSellerName(), "seller");
Map<String, Object> map = new HashMap<>();
map.put("token", token);
map.put("roles", "seller");
return new Result(true, StatusCode.OK, "Login success!", map);
}
return new Result(false, StatusCode.LOGINERROR, "Login failed!");
}
/**
* 根据商家id获取商家信息
* @param id
* @return
*/
@ApiOperation(value="根据商家id获取商家信息")
@GetMapping("/v/{id}")
public Result selectByPrimaryKey(@ApiParam(name = "id", value = "商家id", required = true) @PathVariable String id){
String token = (String) request.getAttribute("claims_seller");
if(token != null && !"".equals(token)){
//拥有权限即可执行查询操作
TbSeller seller = sellerService.selectByPrimaryKey(id);
return new Result(true, StatusCode.OK, seller);
}
return new Result(true, StatusCode.ERROR, "Query failure!");
}