1 BCrypt密码加密
1.1 准备工作
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。
BCrypt强哈希方法 每次加密的结果都不一样。
(1)demo_user工程的pom引入依赖
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
(2)添加配置类
我们在添加了spring security依赖后,所有的地址都被spring security所控制了,我们目前只是需要用到BCrypt密码加密的部分,所以我们要添加一个配置类,配置为所有地址都可以匿名访问。
/**
*安全配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
(3)修改demo_user工程的Application, 配置bean
@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
1.2 用户密码加密
1.2.1 新增用户密码加密
修改demo_user工程的UserService
...
@Autowired
BCryptPasswordEncoder encoder;
public void add(User user) {
user.setId(idWorker.nextId()+""); //主键值
//密码加密
String newpassword = encoder.encode(user.getPassword());//加密后的密码
user.setPassword(newpassword);
userDao.save(user);
}
1.2.2 用户登陆密码校验
修改demo_user工程的UserService
...
/**
* 根据登陆名和密码查询
* @param loginname
* @param password
* @return
*/
public User findByLoginnameAndPassword(String loginname, String password){
User user = userDao.findByLoginname(loginname);
if(user!=null && encoder.matches(password,user.getPassword())){
return user;
}else{
return null;
}
}
...
2 基于JWT的Token认证机制实现
2.1 什么是JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
2.2 JWT组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
- 头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
- 载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
(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
- 签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
(1)header (base64后的)
(2)payload (base64后的)
(3)secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符 串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第 三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用 来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
2.3 JJWT快速入门
2.3.1 token的创建
(1)创建maven工程,引入依赖
...
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
...
(2)创建类CreateJwtTest,用于生成token
...
public class CreateJwtTest {
public static void main(String[] args) {
JwtBuilder builder= Jwts.builder().setId("111")
.setSubject("小明")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"wmg");
System.out.println(builder.compact());
}
}
...
setIssuedAt用于设置签发时间
signWith用于设置签名秘钥
(3)测试运行,输出如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。
2.3.2 token的解析
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户 端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一 样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
创建ParseJwtTest
...
public class ParseJwtTest {
public static void main(String[] args) {
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk";
Claims claims = Jwts.parser().setSigningKey("wmg").parseClaimsJws(token).getBody();
System.out.println("id:" + claims.getId());
System.out.println("subject:" + claims.getSubject());
System.out.println("IssuedAt:" + claims.getIssuedAt());
}
}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证 token
2.3.3 token过期校验
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间
创建CreateJwtTest2
...
public class CreateJwtTest2 {
public static void main(String[] args) {
//为了方便测试,我们将过期时间设置为1分钟
long now = System.currentTimeMillis();//当前时间
long exp = now + 1000*60;//过期时间为1分钟
JwtBuilder builder= Jwts.builder().setId("111")
.setSubject("小明")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"wmg")
.setExpiration(new Date(exp));
System.out.println(builder.compact());
}
}
...
setExpiration 方法用于设置过期时间
修改ParseJwtTest
...
public class ParseJwtTest {
public static void main(String[] args) {
String compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTY1NjksImV4cCI6MTUyMzQxNjYyOX0.Tk91b6mvyjpKcldkic8DgXz0zsPFFnRgTgkgcAsa9cc";
Claims claims = Jwts.parser().setSigningKey("wmg").parseClaimsJws(compactJws).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
SimpleDateFormat sdf=new SimpleDateFormat("yyyy‐MM‐dd hh:mm:ss");
System.out.println("签发时间:"+sdf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sdf.format(claims.getExpiration()));
System.out.println("当前时间:"+sdf.format(new Date()));
}
}
...
测试运行,当未过期时可以正常读取,当过期时会引发io.
jsonwebtoken.ExpiredJwtException异常。
Exception in thread "main" io.jsonwebtoken.ExpiredJwtException: JWT expired at 2019‐06‐08T21:44:55+0800. Current time: 2019‐06‐08T21:44:56+0800 at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:365) at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:458) at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java :518) at cn.itcast.demo.ParseJwtTest.main(ParseJwtTest.java:13)
2.3.4 自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角 色)可以定义自定义claims 创建CreateJwtTest3
...
public class CreateJwtTest3 {
public static void main(String[] args){
//为了方便测试,我们将过期时间设置为1分钟
long now = System.currentTimeMillis();//当前时间
long exp = now + 1000*60;//过期时间为1分钟
JwtBuilder builder= Jwts.builder().setId("111")
.setSubject("小明")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"wmg")
.setExpiration(new Date(exp))
.claim("roles","admin")
.claim("logo","logo.png");
System.out.println( builder.compact() );
}
}
...
修改ParseJwtTest
...
public class ParseJwtTest {
public static void main(String[] args) {
String compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTczMjMsImV4cCI6MTUyMzQxNzM4Mywicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJsb2dvLnBuZyJ9.b11p4g4rE94rqFhcfzdJTPCORikqP_1zJ1MP8KihYTQ";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJws).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("roles:"+claims.get("roles"));
System.out.println("logo:"+claims.get("logo"));
SimpleDateFormat sdf=new SimpleDateFormat("yyyy‐MM‐dd hh:mm:ss");
System.out.println("签发时间:"+sdf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sdf.format(claims.getExpiration()));
System.out.println("当前时间:"+sdf.format(new Date()) );
}
}
3 微服务鉴权
3.1 JWT工具类编写
(1)demo_common工程引入依赖(考虑到工具类的通用性)
...
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
...
(2)修改demo_common工程,创建util.JwtUtil
...
@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();
}
}
(3)修改demo_user工程的application.yml, 添加配置
jwt:
config:
key: wmg
ttl: 360000
3.2 管理员登陆后台签发token
(1)配置bean .修改demo_user工程Application类
@Bean
public JwtUtil jwtUtil(){
return new util.JwtUtil();
}
(2)修改AdminController的login方法
...
@Autowired
private JwtUtil jwtUtil;
/**
* 用户登陆
* @param loginname
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map<String,String> loginMap){
Admin admin = adminService.findByLoginnameAndPassword(loginMap.get("loginname"),loginMap.get("password"));
if(admin!=null){
//生成token
String token = jwtUtil.createJWT(admin.getId(),admin.getLoginname(), "admin");
Map map=new HashMap();
map.put("token",token);
map.put("name",admin.getLoginname());//登陆名
return new Result(true,StatusCode.OK,"登陆成功",map);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错误");
}
}
测试运行结果
{
"flag": true,
"code": 20000,
"message": "登陆成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5ODQzMjc1MDc4ODI5MzgzNjgiLCJzdWIiOiJ4aWF vbWkiLCJpYXQiOjE1MjM1MjQxNTksInJvbGVzIjoiYWRtaW4iLCJleHAiOjE1MjM1MjQ1MTl9 ._YF3oftRNTbq9WCD8Jg1tqcez3cSWoQiDIxMuPmp73o",
"name": "admin"
}
}
3.3 删除用户功能鉴权
前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格+token
(1)修改UserController的findAll方法 ,判断请求中的头信息,提取token并验证权限。
...
@Autowired
private HttpServletRequest request;
/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
String authHeader = request.getHeader("Authorization");//获取头信息
if(authHeader==null){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
if(!authHeader.startsWith("Bearer ")){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
String token=authHeader.substring(7);//提取token
Claims claims = jwtUtil.parseJWT(token);
if(claims==null){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
if(!"admin".equals(claims.get("roles"))){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}
3.4 使用拦截器方式实现token鉴权
如果我们每个方法都去写一段代码,冗余度太高,不利于维护,那如何做使我们的代码看起来更清爽呢?我们可以将这段代码放入拦截器去实现
3.4.1 添加拦截器
Spring为我们提供了org
.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器, 继承此类,可以非常方便的实现自己的拦截器。他有三个方法:
分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲染)、返回处理(已经渲染了页面)
- 在preHandle中,可以进行编码、安全控制等处理;
- 在postHandle中,有机会修改ModelAndView;
- 在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。
(1)创建拦截器类。创建 com.demo.user.filter.JwtFilter
...
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {
System.out.println("经过了拦截器");
return true;
}
}
...
(2)配置拦截器类,创建com.demo.user.ApplicationConfig
...
@Configuration
public class ApplicationConfig extends WebMvcConfigurationSupport {
@Autowired
private JwtFilter jwtFilter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtFilter).
addPathPatterns("/**").
excludePathPatterns("/**/login");
}
}
...
3.4.2 拦截器验证token
(1)修改拦截器类 JwtFilter
...
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {
System.out.println("经过了拦截器");
final String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
final String token = authHeader.substring(7); // The part after "Bearer "
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;
}
}
(2)修改UserController的delete方法
/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
Claims claims=(Claims) request.getAttribute("admin_claims");
if(claims==null){
return new Result(true,StatusCode.ACCESSRROR,"无权访问");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}