SpringCloud学习笔记(九)BCrypt密码加密与微服务鉴权JWT(JSON WEB TOKEN)

登录方式:

有状态:将用户的登录信息存到服务器端

无状态:服务器端不进行登记用户的登录信息

 

第一 BCrypt密码加密

1.1 准备工作 


任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。
BCrypt强哈希方法 每次加密的结果都不一样。

(1)tensquare_user工程的pom引入依赖

<dependency>
   <groupId>org.springframework.boot</groupId>             
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>


(2)添加配置类  (资源/工具类中提供)
我们在添加了spring security依赖后,所有的地址都被spring security所控制了,我们目前只是需要用到BCrypt密码加密的部分,所以我们要添加一个配置类,配置为所有地址都可以匿名访问。

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;
/**
 * @program: 
 * @description: security安全配置类
 * @author: smileTimLi
 * @create: 2019-02-26 00:24
 *  * 安全配置类  
 */
@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();
    }
}

(3)修改tensquare_user工程的Application,  配置bean 

  @Bean
  public BCryptPasswordEncoder bcryptPasswordEncoder(){      
     return new BCryptPasswordEncoder();       
  }   

(4)进行密码加密

admin.setPassword(bCryptPasswordEncoder.encode(admin.getPassword()));

(5)用户登录判断密码是否相同

public User findByMobileAndPassword(String mobile,String password){
		User user = userDao.findByMobile(mobile);
		System.out.println("=======bcrypt=======" + bCryptPasswordEncoder.matches(password,user.getPassword()));
		if (user != null && bCryptPasswordEncoder.matches(user.getPassword(),password)) {
            return user;
		} else {
            return null;
		}
	}

其中的matches,传入时第一个为用户传入的密码、encodedPassword为注册时填写的密码

 public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

第二部分 JWT生成

第一 Token机制相对于Cookie机制又有什么好处呢?

1、支持跨域访问 ,cookie是不允许跨域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息是通过HTTP头传输

2、无状态(也称:服务端可扩展行:Token机制不需要在服务端存储session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息

3、更合适CDN(Content Delivery Network):可以通过内容分发网络请求服务端的所有资料(如:JavaScript、Html、图片等),而你的服务端只需要提供API即可

4、去耦:去耦: 不需要绑定到一个特定的身份验证方案

5、更适合移动应用:当你的客户端是一个原生平台,可以采用Token认证

6、CSRF:因为不在依赖于cookie所以不需要考虑对CSRF(跨站请求伪造)的防范

7、 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256 计算 的Token验证和解析要费时得多. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要 为登录页面做特殊处理.


基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft). 

第二 JWT的组成

1、JWT有三部分组成:头部(加密的算法)、载荷、签名

头部用于描述关于该JWT的最基本的信息,jwt的第一部分

例如其类型以及签名所用的算法等。

这也可以 被表示成一个JSON对象。
 {"typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
 

2.载荷

是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包 含三个部分


(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

3.签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成

header  (base64后的)

playload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符 串,然后通过header中声明的加密方式

进行加盐secret组合加密,然后就构成了jwt的第 三部分

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q

第三 Java的JJWT实现JW

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界 面,隐藏了它的大部分复杂性。 

一、Token的创建

1、引入依赖

<dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt</artifactId>

    <version>0.6.0</version>

</dependency>

2.生成Token   ---- 每次运行生成的都不同,因为签发时间不同

JwtBuilder jwtBuilder = Jwts.builder()
        .setId("999")
        .setSubject("小白")    ---   对应载荷的sub
        .setIssuedAt(new Date())
        .signWith(SignatureAlgorithm.HS256,"ItSalt")
        .setExpiration(new Date(new Date().getTime() + 60000))
        .claim("roles","admin");   ---  自定义角色

System.out.println(jwtBuilder.compact());

setIssuedAt用于设置签发时间

signWith用于设置签名秘钥(签名算法HS256)

其中IsSalt是盐

二、token的解析
       ​ 我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户 端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一 样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息 查询数据库返回相应的结果。

解析token

String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5OTkiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTE1OTMyNDcsImV4cCI6MTU1MTU5MzMwNywicm9sZXMiOiJhZG1pbiJ9.FbIsitGS85p2CMzChzXHu0YH5a4ZuCUi8Pq_I4ZKZvQ";
Claims claims = Jwts.parser()
        .setSigningKey("ItSalt")
        .parseClaimsJws(token)
        .getBody();
System.out.println("id:" + claims.getId());
System.out.println("subject:" + claims.getSubject());
System.out.println("IssuedAt:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((claims.getIssuedAt())));
System.out.println("expiration:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((claims.getExpiration())));
System.out.println(claims.get("roles"));

三、封装JWTUtil

package util;

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;

/**
 * Created by Administrator on 2018/4/11.
 */
@ConfigurationProperties("jwt.config")
public class JwtUtil {

    private String key ;

    private long ttl ;//一个小时

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public long getTtl() {
        return ttl;
    }

    public void setTtl(long ttl) {
        this.ttl = ttl;
    }

    /**
     * 生成JWT
     *
     * @param id
     * @param subject
     * @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 (ttl > 0) {
            builder.setExpiration( new Date( nowMillis + ttl));
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     * @param jwtStr
     * @return
     */
    public Claims parseJWT(String jwtStr){
        return  Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();
    }

}

用户模块引入

application.yml

jwt:
  config:
   key: itcast     #指定盐
   ttl: 36000000   #指定盐过期时间

 adminLoginController 

@RequestMapping(value = "/login",method = RequestMethod.POST)
public Result login(@RequestBody Map<String,String> loginMap){
   Admin admin1 = adminService.findByLoginnameAndPassword(loginMap.get("loginname"),loginMap.get("password"));
   if (ObjectUtils.isEmpty(admin1)) {
      return new Result(false,StatusCode.LOGINERROR,"用户或密码错误");
   }
   //    使用前后端可以通话的操作,采用jwt实现
   //    生成令牌
   String token = jwtUtil.createJWT(admin1.getId(),admin1.getLoginname(),"admin");
   return new Result(true,StatusCode.OK,"登录成功",token);
}

四 用户功能权限鉴权

 删除用户功能鉴权 
public void deleteById(String id) {

		String header = request.getHeader("Authorization");
		if (StringUtils.isEmpty(header)) {
			throw new RuntimeException("权限不足");
		}
		if (!header.startsWith("Bearer ")) {
			throw new RuntimeException("权限不足");
		}
		try{
			String token = header.substring(7);
			Claims claims = jwtUtil.parseJWT(token);
			if (!claims.get("roles").equals("admin")) {
				throw new RuntimeException("权限不足");
			}
		} catch (Exception e){
			throw new RuntimeException("权限不足");
		}
		userDao.deleteById(id);
	}

只有管理员的权限才能删除用户

五 通过拦截器实现用户鉴权

新建JwtInterceptor

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println("经过了过滤器");

        //  无论如何都放行,具体能不能操作还要在具体的操作中取判断
        //  拦截器只是负责把头请求中包含token的令牌进行一个解析验证
        final String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            final String token = authHeader.substring(7);
            try {
                Claims claims = jwtUtil.parseJWT(token);
                if (claims != null) {
                    if ("admin".equals(claims.get("roles"))){
                        //   管理员
                        request.setAttribute("admin_claims",claims);
                    }
                    if ("user".equals(claims.get("roles"))){
                        //   用户
                        request.setAttribute("user_claims",claims);
                    }
                }
            } catch (Exception e){
                throw new RuntimeException("令牌不正确");
            }
        }
        return true;
    }
}

新建InterceptorConfig

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login/**");
    }
}

 删除用户

public void deleteById(String id) {

		Claims claims = (Claims) request.getAttribute("admin_claims");
		if (claims == null) {
			throw new RuntimeException("权限不足");
		}
		userDao.deleteById(id);
	}

六 新增问题模块

添加拦截和jwt.config配置,在请求头添加用户登陆的头信息

/**
	 * 增加
	 * @param problem
	 */
	@RequestMapping(method=RequestMethod.POST)
	public Result add(@RequestBody Problem problem  ){
		Claims claim = (Claims) request.getAttribute("user_claims");
        if (claim == null) {
           return new Result(false,StatusCode.ACCESSERROR,"权限不足");
		}

		problemService.add(problem);
		return new Result(true,StatusCode.OK,"增加成功");
	}

猜你喜欢

转载自blog.csdn.net/qq_35275233/article/details/87927551