密码加密与微服务鉴权JWT

博客学习目标

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

二、使用 JWT 鉴权认证。javascript

1、BCrypt 密码加密

一、常见的加密方式

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

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

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

开发工具:IDEA、Java八、Postman前端

引入依赖java

<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 发送用户注册请求(以下图),在查询数据库可看到用户密码已加密。

5.png

使用 Postman 发送用户登录请求(以下图),返回登录成功提示。
4.png
所有示例代码已经上传到 github ,文末可获取地址。

2、常见的认证机制

2.一、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让用户能够受权第三方网站访问他们存储在另外服务提供者的某些特定信
息,而非全部内容。mysql

下面是OAuth2.0的流程:git

2.png

这种基于OAuth的认证机制适用于我的消费者类的互联网产品,如社交类APP等应
用,可是不太适合拥有自有认证权限管理的企业应用。github

2.4 Token Auth

使用基于 Token 的身份验证方法,在服务端不须要存储用户的登陆记录。大概的流程以下:web

  1. 客户端使用用户名跟密码请求登陆。
  2. 服务端收到请求,去验证用户名与密码。
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端。
  4. 客户端收到 Token 之后能够把它存储起来,好比放在 Cookie 里。
  5. 客户端每次向服务端请求资源的时候须要带着服务端签发的 Token。
  6. 服务端收到请求,而后去验证客户端请求里面带着的 Token,若是验证成功,就向
    客户端返回请求的数据。

下面是Token Auth 的流程:算法

3.png

重点:Token机制相对于Cookie机制的优缺点?spring

  • 支持跨域访问: 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 作功能测试的时候,再也不须要
    为登陆页面作特殊处理.

3、什么是 JSON Web Token(JWT)

JWT 格式组成:头部+载荷+签名 ( header + payload + signature )
头部(Header)
头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。能够被表示成一个 JSON 对象。例如如下在头部指明了签名算法是HS256算法。咱们进行BASE64编码如下内容: {"typ":"JWT","alg":"HS256"},获得编码后的字符串以下:

JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=sql

小知识: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了。

4、案例代码演示

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

需求:删除用户(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 步骤

一、和上面同样使用 Postman 注册一个 Admin 帐户

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

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

源码地址

GitHub地址: https://github.com/RookieMZL/...

欢迎你们指教,提出意见。

相关文章
相关标签/搜索