密码加密与微服务鉴权JWT

BCrypt密码加密

Spring Security提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强
哈希方法来加密密码。
BCrypt强哈希方法 每次加密的结果都不一样。
(1)tensquare_user工程的pom引入依赖


org.springframework.boot
spring‐boot‐starter‐security

(2)添加配置类

/**
* 安全配置类
*/
@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();
}
}

(3)修改tensquare_user工程的Application, 配置bean

@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
新增管理员密码加密

修改tensquare_user工程的AdminService

@Autowired
BCryptPasswordEncoder encoder;
public void add(Admin admin) {
admin.setId(idWorker.nextId()+""); //主键值
//密码加密
String newpassword = encoder.encode(admin.getPassword());//加密后
的密码
admin.setPassword(newpassword);
adminDao.save(admin);
}
管理员登陆密码校验

(1)AdminDao增加方法定义
public Admin findByLoginname(String loginname);
(2)AdminService增加方法

/**
* 根据登陆名和密码查询
* @param loginname
* @param password
* @return
*/
public Admin findByLoginnameAndPassword(String loginname, String
password){
Admin admin = adminDao.findByLoginname(loginname);
if( admin!=null && encoder.matches(password,admin.getPassword()))
{
return admin;
}else{
return null;
}
}

(3)AdminController增加方法

/**
* 用户登陆
* @param loginname
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map loginMap){
Admin admin =
adminService.findByLoginnameAndPassword(loginMap.get("loginname"),
loginMap.get("password"));
if(admin!=null){
return new Result(true,StatusCode.OK,"登陆成功");
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错
误");
}

用户注册密码加密

(4)修改tensquare_user工程的UserService 类,引入BCryptPasswordEncoder

@Autowired
BCryptPasswordEncoder encoder;

(5)修改tensquare_user工程的UserService 类的add方法,添加密码加密的逻辑

/**
* 增加
* @param user
* @param code
*/
public void add(User user,String code) {
........
........
........
//密码加密
String newpassword = encoder.encode(user.getPassword());//加密后的
密码
user.setPassword(newpassword);
userDao.save(user);
}

(4)测试运行后,添加数据

{
"mobile": "13901238899",
"password": "123123"
}

数据库中的密码为以下形式

$2a$10$a/EYRjdKwQ6zjr0/HJ6RR.rcA1dwv1ys7Uso1xShUaBWlIWTyJl5S
用户登陆密码判断

(1)修改tensquare_user工程的UserDao接口,增加方法定义

/**
* 根据手机号查询用户
* @param mobile
* @return
*/
public User findByMobile(String mobile);

(2)修改tensquare_user工程的UserService 类,增加方法

/**
* 根据手机号和密码查询用户
* @param mobile
* @param password
* @return
*/
public User findByMobileAndPassword(String mobile,String password){
User user = userDao.findByMobile(mobile);
if(user!=null && encoder.matches(password,user.getPassword())){
return user;
}else{
return null;
}
}

(4)修改tensquare_user工程的UserController类,增加login方法

/**
* 用户登陆
* @param mobile
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(String mobile,String password){
User user = userService.findByMobileAndPassword(mobile,password);
if(user!=null){
return new Result(true,StatusCode.OK,"登陆成功");
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错
误");
}
}
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 做功能测试的时候,不再需要
为登录页面做特殊处理.

基于JWT的Token认证机制实现

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以
被表示成一个JSON对象。

{"typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 我们进行BASE64编码
(1)标准中注册的声明(建议但不强制使用)

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

JWT快速入门 token的创建

(1)创建maven工程,引入依赖


io.jsonwebtoken
jjwt
0.6.0

(2)创建类CreateJwtTest,用于生成token

public class CreateJwtTest {
public static void main(String[] args) {
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"itcast");
System.out.println( builder.compact() );
}
}

setIssuedAt用于设置签发时间
signWith用于设置签名秘钥
(3)测试运行,输出如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0M
TM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk
token的解析

客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一
样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息
查询数据库返回相应的结果。
创建ParseJwtTest

public class ParseJwtTest {
public static void main(String[] args) {
String
token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiO
jE1MjM0MTM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk";
Claims claims =
Jwts.parser().setSigningKey("itcast").parseClaimsJws(token).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("IssuedAt:"+claims.getIssuedAt());
}
}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析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(".setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,.setExpiration(new Date(exp));
System.out.println( builder.compact() );
}
}

setExpiration 方法用于设置过期时间
修改ParseJwtTest

public class ParseJwtTest {
public static void main(String[] args) {
String
compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJp
YXQiOjE1MjM0MTY1NjksImV4cCI6MTUyMzQxNjYyOX0.Tk91b6mvyjpKcldkic8DgXz0zsPFF
nRgTgkgcAsa9cc";
Claims claims =
Jwts.parser().setSigningKey("itcast").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异常。

自定义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("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"itcast")
.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_nmb0iLCJp
YXQiOjE1MjM0MTczMjMsImV4cCI6MTUyMzQxNzM4Mywicm9sZXMiOiJhZG1pbiIsImxvZ28iO
iJsb2dvLnBuZyJ9.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()) );
}
}
实战微服务鉴权
JWT工具类编写

(1)tensquare_common工程引入依赖(考虑到工具类的通用性)


io.jsonwebtoken
jjwt
0.6.0

(2)修改tensquare_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)修改tensquare_user工程的application.yml, 添加配置

jwt:
config:
key: itcast
ttl: 360000

管理员登陆后台签发token

(1)配置bean .修改tensquare_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 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"
}
}

删除用户功能鉴权

需求:删除用户,必须拥有管理员权限,否则不能删除。
前后端约定:前端请求微服务时需要添加头信息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,"删除成功");
}

拦截器方式实现token鉴权

(1)创建过滤器/拦截器类。创建 com.tensquare.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.tensquare.user.ApplicationConfig

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

过滤器验证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.DELEpublic Result delete(@PathVariable String id ){
Claims claims=(Claims) request.getAttribute("admin_cif(claims==null){
return new Result(true,StatusCode.ACCESSRROR,"无}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}
发布信息验证Token 用户登陆签发 JWT

(2)修改UserController,引入JwtUtil 修改login方法 ,返回token,昵称,头像等信息

@Autowired
private JwtUtil jwtUtil;
/**
* 用户登陆
* @param mobile
* @param password
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map loginMap){
User user =
userService.findByMobileAndPassword(loginMap.get("mobile"),loginMap.get("
password"));
if(user!=null){
String token = jwtUtil.createJWT(user.getId(),
user.getNickname(), "user");
Map map=new HashMap();
map.put("token",token);
map.put("name",user.getNickname());//昵称
map.put("avatar",user.getAvatar());//头像
return new Result(true,StatusCode.OK,"登陆成功",map);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错
误");
}
}

(4)测试运行 http://localhost:9008/user/login (POST) ,结果为如下形式

{
"flag": true,
"code": 20000,
"message": "登陆成功",
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5ODM5ODc0ODk1MDcyNTAxNzYiLCJpYXQiOjE1MjM
1MDIzMDEsInJvbGVzIjoidXNlciIsImV4cCI6MTUyMzUwMjY2MX0.QH‐
WMRgIAxHDcYReH7patg7qkcjc4ZuyDbHaIah‐spQ"
},
"name":"小雅",
"avatar":"......."
}

问答模块 -发布问题

(1)修改tensquare_qa工程的QaApplication,增加bean

@Bean
public JwtUtil jwtUtil(){
return new util.JwtUtil();
}

(2)tensquare_qa工程配置文件application.yml增加配置:

jwt:
config:
key: itcast

(3)增加拦截器类 (参考上面的拦截器代码,复制配置过的代码)
(4)增加配置类ApplicationConfig (参考上面的配置类代码,复制配合过的代码)
(5)修改ProblemController的add方法

@Autowired
private HttpServletRequest request;
/**
* 发布问题
* @param problem
*/
@RequestMapping(method=RequestMethod.POST)
public Result add(@RequestBody Problem problem ){
Claims claims=(Claims)request.getAttribute("user_claims");
if(claims==null){
return new Result(false,StatusCode.ACCESSERROR,"无权访问");
}
problem.setUserid(claims.getId());
problemService.add(problem);
return new Result(true,StatusCode.OK,"增加成功");
}

你可能感兴趣的:(密码加密与微服务鉴权JWT)