密码加密与微服务鉴权JWT

一、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!");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值