1、BCrypt密码加密
1.1、 准备工作
Ⅰ、任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。
Ⅱ、Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。
Ⅲ、BCrypt强哈希方法 每次加密的结果都不一样。
(1)user工程的pom引入依赖
org.springframework.boot
spring-boot-starter-security
(2)添加配置类
在添加了 spring security 依赖后,所有的地址都被 spring security 所控制了,我们目前只是需要用到 BCrypt 密码加密的部分,所以我们要添加一个配置类,配置为所有地址都可以匿名访问。
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
/**
* authorizeRequests:所有security全注解配置实现的开端,表示开始说明需要的权限;
* 需要的权限分两部分,第一部分是拦截的路径,第二部分访问该路径需要的权限;
* antMatchers:表示拦截什么路径,permitAll 任何权限都可以访问,直接放行所有;
* anyRequest():任何请求,authenticated 认证后才能访问;
* .and().csrf().disable():固定写法,表示使csrf拦截失效。
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
(3)修改user工程的Application, 配置bean
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
1.2、管理员密码加密
1.2.1 新增管理员密码加密
修改user工程的AdminService
@Autowired
private BCryptPasswordEncoder encoder;
/**
* 增加
* @param admin
*/
public void add(Admin admin) {
admin.setId( idWorker.nextId()+"" );
//密码加密
admin.setPassword(encoder.encode(admin.getPassword()));
adminDao.save(admin);
}
1.2.2、管理员登陆密码校验
(1)AdminDao增加方法定义
//根据用户名查询
public Admin findByLoginname(String loginname);
(2)AdminService增加方法
/*
* @Description: //TODO 用户登录
* @Param: [admin]
* @return: com.tensquare.user.pojo.Admin
*/
public Admin login(Admin admin) {
//现根据用户名查询对象
Admin adminLogin = adminDao.findByLoginname(admin.getLoginname());
//用数据库中查询出来得密码和用户输入得密码比对
if (adminLogin != null && encoder.matches(admin.getPassword(),adminLogin.getPassword())){
//登录成功
return adminLogin;
}
//登录失败
return null;
}
(3)AdminController增加方法
/**
* @Description: //TODO 用户登录
* @Param: []
* @return: entity.Result
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin){
admin = adminService.login(admin);
if (admin == null){
return new Result(false, StatusCode.LOGINERROR, "登录失败");
}
//使得前后端通话,后续完善
return new Result(true, StatusCode.OK, "登录成功", map);
}
1.3、用户密码加密
1.3.1、用户注册密码加密
(1)、修改user工程的UserService 类,引入BCryptPasswordEncoder
@Autowired
private BCryptPasswordEncoder encoder;
(2)、修改user工程的UserService 类的add方法,添加密码加密的逻辑
/**
* 增加
* @param user
*/
public void add(User user) {
user.setId( idWorker.nextId()+"" );
//密码加密
user.setPassword(encoder.encode(user.getPassword()));
userDao.save(user);
}
(3)测试运行后,添加数据
{
"mobile": "13901238899" ,
"password": "123456"
}
数据库中的密码为以下形式
$2a$10$a/EYRjdKwQ6zjr0/HJ6RR.rcA1dwv1ys7Uso1xShUaBWlIWTyJl5S
1.3.2 用户登陆密码判断
(1)修改user工程的UserDao接口,增加方法定义
//根据用户名查询
public User findByMobile(String mobile);
(2)修改user工程的UserService 类,增加方法
/**
* 增加
* @param user
*/
public void add(User user) {
user.setId( idWorker.nextId()+"" );
//密码加密
user.setPassword(encoder.encode(user.getPassword()));
userDao.save(user);
}
(3)修改tensquare_user工程的UserController类,增加login方法
/*
* @Description: //TODO 用户登录
* @Param: [user]
* @return: entity.Result
*/
@PostMapping("/login")
public Result login(@RequestBody User user){
user = userService.login(user.getMobile(), user.getPassword());
if (user == null){
return new Result(false, StatusCode.LOGINERROR, "登录失败");
}
return new Result(true, StatusCode.OK, "登录成功", map);
}
(4)使用刚才新增加的账号进行测试,查看返回结果
2 常见的认证机制
2.1Token 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).
3 基于JWT的Token认证机制实现
3.1、什么是JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用 户和服务器之间传递安全可靠的信息。
3.2、JWT组成
由三部分组成,头部、载荷与签名。
(1)、头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 进行BASE64编 码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
(2)、载荷(playload)
载荷就是存放有效信息的地方。这些有效信息包 含三个部分:
- 标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
- 私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64 是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行base64编码,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
(3)、签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符 串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第 三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用 来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流 露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
4、Java的JJWT实现JWT
4.1、什么是JJWT
JJWT是一个提供端到端的JWT创建和验证的Java库。
4.2、JJWT使用
4.2.1、token的创建
(1)创建maven工程,引入依赖
io.jsonwebtoken
jjwt
0.9.0
(2)创建类CreateJwtTest,用于生成token
public class CreatJwt {
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("666") //用户ID
.setSubject("小明") //用户名
.setIssuedAt(new Date()) //登录时间
.signWith(SignatureAlgorithm.HS256, "justIT") //签名
.setExpiration(new Date(new Date().getTime()+300000)//设置过期时间
.claim("roles","admin"); //自定义项
System.out.println(jwtBuilder.compact());
}
}
setIssuedAt用于设置签发时间;
setExpiration用于设置过期时间;
signWith用于设置签名秘钥;
claim用于设置自定义项;
有很多时候,我们并不希望签发的token是永久生效的,所以我们会为token添加一个 过期时间setExpiration。
当未过期时可以正常读取,当过期时会引发 io.jsonwebtoken.ExpiredJwtException异常。
如果想存储更多的信息(例如角 色)可以定义自定义claims。
(3)测试运行,输出如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_mmI4iLCJpYXQiOjE1NjY3ODI3MzMsImV4cCI6MTU2Njc4MzAzMywicm9sZXMiOiJhZG1pbiJ9.GZieJm3zfJgu2cerYS0zIsCphdAjQm70gT-l39yxP2I
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
4.2.2、token的解析
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户 端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一 样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息 查询数据库返回相应的结果。
(1)创建ParseJwt
public class ParseJwt {
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlsI_mmI4iLCJpYXQiOjE1NjY3ODI3MzMsImV4cCI6MTU2Njc4MzAzMywicm9sZXMiOiJhZG1pbiJ9.GZieJm3zfJgu2cerYS0zIsCphdAjQm70gT-l39yxP2I";
try {
Claims claims = Jwts.parser()
.setSigningKey("justIT")
.parseClaimsJws(token)
.getBody();
System.out.println(claims);
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("roles"));
}catch (Exception e){
System.out.println("签名失效");
}
}
}
后文继续.......