登录方式:
有状态:将用户的登录信息存到服务器端
无状态:服务器端不进行登记用户的登录信息
第一 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,"增加成功");
}