博客学习目标

1、用户注册时候,对数据库中用户的密码进行加密存储(使用 SpringSecurity)。

2、使用 JWT 鉴权认证。

一、BCrypt 密码加密

1、常见的加密方式

任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。

有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security

提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密数据库中用户的密码。BCrypt强哈希方法 每次加密的结果都不一样。

2、是骡子是马拉出来遛遛(代码案例演示)

技术栈:SpringBoot 2.1.6.RELEASE(数据访问层使用 JPA)

开发工具:IDEA、Java8、Postman

引入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 引入 SpringSecurity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- lombok工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

控制层 controller

@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController { @Autowired
private UserService userService; // 用户注册
@RequestMapping(value = "/register", method = RequestMethod.POST)
public Result register(@RequestBody User user) { boolean isRegister = userService.register(user); if (!isRegister) {
return new Result(false, StatusCode.ERROR, "手机号码已经被注册,请直接登陆!");
} return new Result(true, StatusCode.OK, "注册成功!");
} // 用户登陆(限定使用手机号和密码登录)
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result login(@RequestBody User user) { User loginUser = userService.login(user.getMobile(), user.getPassword()); if (null == loginUser) {
return new Result(false, StatusCode.LOGINERROR, "登陆失败,请检查手机号或者密码是否正确.");
} return new Result(true, StatusCode.OK, "登陆成功.");
}
}

业务处理层 service

@Service
public class UserService { @Autowired
private UserDao userDao; @Autowired
private BCryptPasswordEncoder encoder; // 用户注册功能
public boolean register(User user) { User existUser = userDao.findByMobile(user.getMobile()); if (null == existUser) {
user.setId(UUIDUtil.getUUID())
.setPassword(encoder.encode(user.getPassword())) // 密码加密
.setFollowcount(0)
.setFanscount(0)
.setOnline(0L)
.setRegdate(new Date())
.setUpdatedate(new Date())
.setLastdate(new Date()); userDao.save(user); return true;
} return false; } // 用户登陆(限定使用手机号和密码登录)
public User login(String mobile, String password) { User existUser = userDao.findByMobile(mobile); if (null != existUser && encoder.matches(password, existUser.getPassword())) { return existUser;
} return null;
}
}

数据库访问层 dao

public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {

    // 判断用户手机号是否已经注册
User findByMobile(String mobile);
}

启动类注入 BCryptPasswordEncoder

@SpringBootApplication
public class BcryptJwtApplication { public static void main(String[] args) {
SpringApplication.run(BcryptJwtApplication.class, args);
} @Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

SpringSecurity 安全配置类,对路径拦截。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //authorizeRequests 所有 security 全注解配置实现的开端,表示开始说明需要的权限
//需要的权限分两部分,第一部分是拦截的路径,第二部分访问该路径需要的权限
//antMarcher表示拦截说明路径,permitAll任何权限都可以访问,直接放行所有
//anyRequest()任何请求,authenticated认证后才能访问
//.and.csrf.disable(),固定写法,表示使用csrf拦截失败
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}

使用 Postman 发送用户注册请求(如下图),在查询数据库可看到用户密码已加密。

使用 Postman 发送用户登陆请求(如下图),返回登陆成功提示。

全部示例代码已经上传到 github ,文末可获取地址。

二、常见的认证机制

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 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程如下:

  1. 客户端使用用户名跟密码请求登录。
  2. 服务端收到请求,去验证用户名与密码。
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端。
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里。
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token。
  6. 服务端收到请求,然后去验证客户端请求里面带着的 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 做功能测试的时候,不再需要

    为登录页面做特殊处理.

三、什么是 JSON Web Token(JWT)

JWT 格式组成:头部+载荷+签名 ( header + payload + signature )

头部(Header)

头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。可以被表示成一个 JSON 对象。例如以下在头部指明了签名算法是HS256算法。我们进行BASE64编码以下内容:{"typ":"JWT","alg":"HS256"},得到编码后的字符串如下:

JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2

的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24

个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中

提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的

完成基于 BASE64 的编码和解码。

载荷(playload)

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

(1) 标准中注册的声明(建议但不强制使用)

  • iss: jwt签发者。
  • sub: jwt所面向的用户。
  • aud: 接收jwt的一方 。
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间 。
  • nbf: 定义在什么时间之前,该jwt都是不可用的.。
  • iat: jwt的签发时间 。
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

(2) 公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.

但不建议添加敏感信息,因为该部分在客户端可解密。

(3) 私有声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64

是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload: {"sub":"1234567890","name":"John Doe","admin":true},然后将其进行base64编码,得到 Jwt 的第二部分如下:JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJKb2huJUEwRG9lJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE

签名(signature)

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

header (base64后的)

payload (base64后的)。

secret。

这个部分需要base64加密后的header和base64加密后的payload使用,连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分如下:(就是使用头部指明的签名算法对已经加密了以后的字符串在进行加密得到第三部分)

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=.JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJKb2huJUEwRG9lJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

知识点1:
Signature 部分是对前两部分的签名,防止数据篡改。首先需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用 Header 里面指定的签名算法,按照下面的公式产生签名HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用(.)分隔,就可以返回给用户。
知识点2:
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

四、案例代码演示

在上面代码基础上继续演示

需求:删除用户(User),必须拥有管理员(Admin)权限,否则不能删除。

前后端约定:前端请求后端时需要添加头信息 Authorization ,内容为Bearer+空格

+token

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>

用户生成 、解析 token 的工具类

@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
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
public Claims parseJWT(String jwtStr) {
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
} }

启动类注入 JwtUtil 工具类

@SpringBootApplication
public class BcryptJwtApplication { public static void main(String[] args) {
SpringApplication.run(BcryptJwtApplication.class, args);
} @Bean
public JwtUtil jwtUtil() {
return new JwtUtil();
} }

创建拦截器类

如果每个方法都去写一段代码验证用户登陆 token 的正确性,冗余度太高不利于维护。我们可以将这段代码放入拦截器去实现同意拦截,再判断用户 token。

Spring为我提供了

org.springframework.web.servlet.handler.HandlerInterceptorAdapter 这个适配器,

继承此类,可以非常方便的实现自己的拦截器。

他有三个方法:

分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲

染)、返回处理(已经渲染了页面)。

在preHandle中,可以进行编码、安全控制等处理;

在postHandle中,有机会修改ModelAndView;

在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。

@Component
public class JwtInterceptor extends HandlerInterceptorAdapter { @Autowired
private JwtUtil jwtUtil; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { System.out.println("经过拦截器"); final String authHeader = request.getHeader("Authorization");//获取头信息
if (authHeader != null && authHeader.startsWith("Bearer ")) { // 注意是 Bearer + 空格
final String token = authHeader.substring(7);
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);
}
}
} return true;
}
}

配置拦截器类

@Configuration
public class ApplicationConfig extends WebMvcConfigurationSupport { @Autowired
private JwtInterceptor jwtInterceptor; protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/**/login");
}
}

控制层 controller

@RestController
@CrossOrigin
@RequestMapping("/admin")
public class AdminController { @Autowired
private AdminService adminService; @Autowired
private JwtUtil jwtUtil; // Admin 用户登陆
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result login(@RequestBody Admin admin) { Admin loginUser = adminService.findByLoginNameAndPassword(admin.getLoginname(), admin.getPassword()); if (null == loginUser) {
return new Result(false, StatusCode.LOGINERROR, "登陆失败,请检查用户名或者密码是否正确");
} // 生成令牌,并且返回给前台
String token = jwtUtil.createJWT(loginUser.getId(), loginUser.getLoginname(), "admin");
Map<String, Object> map = new HashMap<>();
map.put("token", token);
map.put("role", "admin");
map.put("name", loginUser.getLoginname()); return new Result(true, StatusCode.OK, "登陆成功", map);
} // 添加 Admin 用户
@RequestMapping(value = "/add", method = RequestMethod.POST)
public Result add(@RequestBody Admin admin) {
adminService.add(admin);
return new Result(true, StatusCode.OK, "增加成功");
}
}

业务处理层 service

@Service
public class AdminService { @Autowired
private AdminDao adminDao; @Autowired
private BCryptPasswordEncoder encoder; // 根据登陆用户名和密码查询
public Admin findByLoginNameAndPassword(String loginName, String password) { Admin admin = adminDao.findByLoginname(loginName);
if (null != admin && encoder.matches(password, admin.getPassword())) {
return admin;
} return null;
} // 添加管理员
public void add(Admin admin) {
admin.setId(UUIDUtil.getUUID()); // 主键
// 密码加密
String newPassword = encoder.encode(admin.getPassword());
admin.setPassword(newPassword);
adminDao.save(admin);
} }

数据访问层 dao

public interface AdminDao extends JpaRepository<Admin, String>, JpaSpecificationExecutor<Admin> {

    // 管理员登陆校验
Admin findByLoginname(String loginName);
}

修改UserController的delete方法

@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController { @Autowired
private UserService userService; @Autowired
private HttpServletRequest servletRequest; /**
* 删除:删除用户,必须拥有管理员权限,否则不能删除
* <p>
* 前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格+token
*
* @param id
*/
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable String id) { Claims claims = (Claims) servletRequest.getAttribute("admin_claims");
if (null == claims) {
return new Result(true, StatusCode.ACCESSERROR, "无权访问");
}
userService.deleteById(id);
return new Result(true, StatusCode.OK, "删除成功");
} }

测试生成 token 步骤

1、和上面一样使用 Postman 注册一个 Admin 账户

2、使用 Postman 模拟访问登陆,看是否返回 token

3、在使用 Bearer+空格+token,放入头部删除用户,看是否删除成功。

源码地址

GitHub地址: https://github.com/RookieMZL/practice-sample/tree/dev/bcrypt-jwt

欢迎大家指教,提出意见。

密码加密与微服务鉴权JWT的更多相关文章

  1. 密码加密与微服务鉴权JWT详细使用

    [TOC] 1.1.了解微服务状态 微服务集群中的每个服务,对外提供的都是Rest风格的接口,而Rest风格的一个最重要的规范就是:服务的无状态性. 什么是无状态? 1.服务端不保存任何客户端请求者信 ...

  2. JWT对SpringCloud进行系统认证和服务鉴权

    JWT对SpringCloud进行系统认证和服务鉴权 一.为什么要使用jwt?在微服务架构下的服务基本都是无状态的,传统的使用session的方式不再适用,如果使用的话需要做同步session机制,所 ...

  3. spring cloud jwt用户鉴权及服务鉴权

    用户鉴权 客户端请求服务时,根据提交的token获取用户信息,看是否有用户信息及用户信息是否正确 服务鉴权 微服务中,一般有多个服务,服务与服务之间相互调用时,有的服务接口比较敏感,比如资金服务,不允 ...

  4. 畅购商城(八):微服务网关和JWT令牌

    好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 畅购商城(一):环境搭建 畅购商 ...

  5. Spring Cloud OAuth2.0 微服务中配置 Jwt Token 签名/验证

    关于 Jwt Token 的签名与安全性前面已经做了几篇介绍,在 IdentityServer4 中定义了 Jwt Token 与 Reference Token 两种验证方式(https://www ...

  6. shiro jwt 构建无状态分布式鉴权体系

    一:JWT 1.令牌构造 JWT(json web token)是可在网络上传输的用于声明某种主张的令牌(token),以JSON 对象为载体的轻量级开放标准(RFC 7519). 一个JWT令牌的定 ...

  7. SpringCloud微服务实战——搭建企业级开发框架(二十三):Gateway+OAuth2+JWT实现微服务统一认证授权

      OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间.限定 ...

  8. SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统

      业务系统正常运行的稳定性十分重要,作为SpringBoot的四大核心之一,Actuator让你时刻探知SpringBoot服务运行状态信息,是保障系统正常运行必不可少的组件.   spring-b ...

  9. SpringCloud 微服务最佳开发实践

    Maven规范 所有项目必须要有一个统一的parent模块 所有微服务工程都依赖这个parent,parent用于管理依赖版本,maven仓库,jar版本的统一升级维护 在parent下层可以有 co ...

随机推荐

  1. pugixml的使用

    VS项目,头文件处鼠标右键,添加“新建筛选器”,重命名为pugixml,把3个文件添加进来.在用到框架的文件中只需#include"pugixml\pugixml.hpp"即可. ...

  2. spark streaming简单示例

    <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://mave ...

  3. dubbo 漫谈一

    转:腾信视频 阿甘 https://ke.qq.com/course/216518 https://blog.csdn.net/xlgen157387/article/details/51865289 ...

  4. Java 系书籍,,,,,,,,,,,,,

    Java 系书籍 本文仅对每本书做简单介绍,里面的精华我是希望留给各位看官仔细去阅读去琢磨~~ Java 1. <Java核心技术 卷1 基础知识> 2. <Java核心技术 卷II ...

  5. CodeForces 593D Happy Tree Party

    题目链接: http://codeforces.com/problemset/problem/593/D ----------------------------------------------- ...

  6. 记.net3.5离线安装问题

    dism.exe /online /enable-feature /featurename:netfx3 /Source: X:\sourse\sxs pause 相关文件要相同版本的ISO中提取,否 ...

  7. Skyline(6.x)-Web二次开发-1多窗口对比

    一个页面加载多个 TerraExplorer3DWindow 和 SGWorld 等只有第一个能用(即使用 iframe 也是一样) 所以我决定打开两个新页面实现多窗口对比,然后我在<主页面&g ...

  8. Linux 下使用 nohup

    参考: https://www.cnblogs.com/klb561/p/10153834.html ppending output to nohup.out 嗯,证明运行成功,同时把程序运行的输出信 ...

  9. linux远程管理器 - xshell和xftp使用教程(zhuan)

    准备好连接linux服务器的工具,推荐用xshell和xftp. xshell 是一个强大的安全终端模拟软件,它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET ...

  10. 大数据学习笔记之Hadoop(三):MapReduce&YARN

    文章目录 一 MapReduce概念 1.1 为什么要MapReduce 1.2 MapReduce核心思想 1.3 MapReduce进程 1.4 MapReduce编程规范(八股文) 1.5 Ma ...