当前位置: 移动技术网 > IT编程>开发语言>Java > Spring Security结合JWT的方法教程

Spring Security结合JWT的方法教程

2019年07月19日  | 移动技术网IT编程  | 我要评论

概述

众所周知使用 jwt 做权限验证,相比 session 的优点是,session 需要占用大量服务器内存,并且在多服务器时就会涉及到共享 session 问题,在手机等移动端访问时比较麻烦

而 jwt 无需存储在服务器,不占用服务器资源(也就是无状态的),用户在登录后拿到 token 后,访问需要权限的请求时附上 token(一般设置在http请求头),jwt 不存在多服务器共享的问题,也没有手机移动端访问问题,若为了提高安全,可将 token 与用户的 ip 地址绑定起来

前端流程

用户通过 ajax 进行登录得到一个 token

之后访问需要权限请求时附上 token 进行访问

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>title</title>
 <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
 <script type="application/javascript">
  var header = "";
  function login() {
   $.post("http://localhost:8080/auth/login", {
    username: $("#username").val(),
    password: $("#password").val()
   }, function (data) {
    console.log(data);
    header = data;
   })
  }
  function touserpagebtn() {
   $.ajax({
    type: "get",
    url: "http://localhost:8080/userpage",
    beforesend: function (request) {
     request.setrequestheader("authorization", header);
    },
    success: function (data) {
     console.log(data);
    }
   });
  }
 </script>
</head>
<body>
 <fieldset>
  <legend>please login</legend>
  <label>username</label><input type="text" id="username">
  <label>password</label><input type="text" id="password">
  <input type="button" onclick="login()" value="login">
 </fieldset>
 <button id="touserpagebtn" onclick="touserpagebtn()">访问userpage</button>
</body>
</html>

后端流程(spring boot + spring security + jjwt)

思路:

  • 创建用户、权限实体类与数据传输对象
  • 编写 dao 层接口,用于获取用户信息
  • 实现 userdetails(security 支持的用户实体对象,包含权限信息)
  • 实现 userdetailssevice(从数据库中获取用户信息,并包装成userdetails)
  • 编写 jwttoken 生成工具,用于生成、验证、解析 token
  • 配置 security,配置请求处理 与 设置 userdetails 获取方式为自定义的 userdetailssevice
  • 编写 logincontroller,接收用户登录名密码并进行验证,若验证成功返回 token 给用户
  • 编写过滤器,若用户请求头或参数中包含 token 则解析,并生成 authentication,绑定到 securitycontext ,供 security 使用
  • 用户访问了需要权限的页面,却没附上正确的 token,在过滤器处理时则没有生成 authentication,也就不存在访问权限,则无法访问,否之访问成功

编写用户实体类,并插入一条数据

user(用户)实体类

@data
@entity
public class user {
 @id
 @generatedvalue
 private int id;
 private string name;
 private string password;
 @manytomany(cascade = {cascadetype.refresh}, fetch = fetchtype.eager)
 @jointable(name = "user_role", joincolumns = {@joincolumn(name = "uid", referencedcolumnname = "id")}, inversejoincolumns = {@joincolumn(name = "rid", referencedcolumnname = "id")})
 private list<role> roles;
} 

role(权限)实体类

@data
@entity
public class role {
 @id
 @generatedvalue
 private int id;
 private string name;
 @manytomany(mappedby = "roles")
 private list<user> users;
}

插入数据

user 表

id name password
1 linyuan 123

role 表

id name
1 user

user_role 表

uid rid
1 1

dao 层接口,通过用户名获取数据,返回值为 java8 的 optional 对象

public interface userrepository extends repository<user,integer> {
 optional<user> findbyname(string name);
}

编写 logindto,用于与前端之间数据传输

@data
public class logindto implements serializable {
 @notblank(message = "用户名不能为空")
 private string username;
 @notblank(message = "密码不能为空")
 private string password;
}

编写 token 生成工具,利用 jjwt 库创建,一共三个方法:生成 token(返回string)、解析 token(返回authentication认证对象)、验证 token(返回布尔值)

@component
public class jwttokenutils {
 private final logger log = loggerfactory.getlogger(jwttokenutils.class);
 private static final string authorities_key = "auth";
 private string secretkey;   //签名密钥
 private long tokenvalidityinmilliseconds;  //失效日期
 private long tokenvalidityinmillisecondsforrememberme;  //(记住我)失效日期
 @postconstruct
 public void init() {
  this.secretkey = "linyuanmima";
  int secondin1day = 1000 * 60 * 60 * 24;
  this.tokenvalidityinmilliseconds = secondin1day * 2l;  this.tokenvalidityinmillisecondsforrememberme = secondin1day * 7l;
 }
 private final static long expirationtime = 432_000_000;
 //创建token
 public string createtoken(authentication authentication, boolean rememberme){
  string authorities = authentication.getauthorities().stream()  //获取用户的权限字符串,如 user,admin
    .map(grantedauthority::getauthority)
    .collect(collectors.joining(","));
  long now = (new date()).gettime();    //获取当前时间戳
  date validity;           //存放过期时间
  if (rememberme){
   validity = new date(now + this.tokenvalidityinmilliseconds);
  }else {
   validity = new date(now + this.tokenvalidityinmillisecondsforrememberme);
  }
  return jwts.builder()         //创建token令牌
    .setsubject(authentication.getname())   //设置面向用户
    .claim(authorities_key,authorities)    //添加权限属性
    .setexpiration(validity)      //设置失效时间
    .signwith(signaturealgorithm.hs512,secretkey) //生成签名
    .compact();
 }
 //获取用户权限
 public authentication getauthentication(string token){
  system.out.println("token:"+token);
  claims claims = jwts.parser()       //解析token的payload
    .setsigningkey(secretkey)
    .parseclaimsjws(token)
    .getbody();
  collection<? extends grantedauthority> authorities =
    arrays.stream(claims.get(authorities_key).tostring().split(","))   //获取用户权限字符串
    .map(simplegrantedauthority::new)
    .collect(collectors.tolist());             //将元素转换为grantedauthority接口集合
  user principal = new user(claims.getsubject(), "", authorities);
  return new usernamepasswordauthenticationtoken(principal, "", authorities);
 }
 //验证token是否正确
 public boolean validatetoken(string token){
  try {
   jwts.parser().setsigningkey(secretkey).parseclaimsjws(token); //通过密钥验证token
   return true;
  }catch (signatureexception e) {          //签名异常
   log.info("invalid jwt signature.");
   log.trace("invalid jwt signature trace: {}", e);
  } catch (malformedjwtexception e) {         //jwt格式错误
   log.info("invalid jwt token.");
   log.trace("invalid jwt token trace: {}", e);
  } catch (expiredjwtexception e) {         //jwt过期
   log.info("expired jwt token.");
   log.trace("expired jwt token trace: {}", e);
  } catch (unsupportedjwtexception e) {        //不支持该jwt
   log.info("unsupported jwt token.");
   log.trace("unsupported jwt token trace: {}", e);
  } catch (illegalargumentexception e) {        //参数错误异常
   log.info("jwt token compact of handler are invalid.");
   log.trace("jwt token compact of handler are invalid trace: {}", e);
  }
  return false;
 }
}

实现 userdetails 接口,代表用户实体类,在我们的 user 对象上在进行包装,包含了权限等性质,可以供 spring security 使用

public class myuserdetails implements userdetails{
 private user user;
 public myuserdetails(user user) {
  this.user = user;
 }
 @override
 public collection<? extends grantedauthority> getauthorities() {
  list<role> roles = user.getroles();
  list<grantedauthority> authorities = new arraylist<>();
  stringbuilder sb = new stringbuilder();
  if (roles.size()>=1){
   for (role role : roles){
    authorities.add(new simplegrantedauthority(role.getname()));
   }
   return authorities;
  }
  return authorityutils.commaseparatedstringtoauthoritylist("");
 }
 @override
 public string getpassword() {
  return user.getpassword();
 }
 @override
 public string getusername() {
  return user.getname();
 }
 @override
 public boolean isaccountnonexpired() {
  return true;
 }
 @override
 public boolean isaccountnonlocked() {
  return true;
 }
 @override
 public boolean iscredentialsnonexpired() {
  return true;
 }
 @override
 public boolean isenabled() {
  return true;
 }
}

实现 userdetailsservice 接口,该接口仅有一个方法,用来获取 userdetails,我们可以从数据库中获取 user 对象,然后将其包装成 userdetails 并返回

@service
public class myuserdetailsservice implements userdetailsservice {
 @autowired
 userrepository userrepository;
 @override
 public userdetails loaduserbyusername(string s) throws usernamenotfoundexception {
  //从数据库中加载用户对象
  optional<user> user = userrepository.findbyname(s);
  //调试用,如果值存在则输出下用户名与密码
  user.ifpresent((value)->system.out.println("用户名:"+value.getname()+" 用户密码:"+value.getpassword()));
  //若值不再则返回null
  return new myuserdetails(user.orelse(null));
 }
}

编写过滤器,用户如果携带 token 则获取 token,并根据 token 生成 authentication 认证对象,并存放到 securitycontext 中,供 spring security 进行权限控制

public class jwtauthenticationtokenfilter extends genericfilterbean {
 private final logger log = loggerfactory.getlogger(jwtauthenticationtokenfilter.class);
 @autowired
 private jwttokenutils tokenprovider;
 @override
 public void dofilter(servletrequest servletrequest, servletresponse servletresponse, filterchain filterchain) throws ioexception, servletexception {
  system.out.println("jwtauthenticationtokenfilter");
  try {
   httpservletrequest httpreq = (httpservletrequest) servletrequest;
   string jwt = resolvetoken(httpreq);
   if (stringutils.hastext(jwt) && this.tokenprovider.validatetoken(jwt)) {   //验证jwt是否正确
    authentication authentication = this.tokenprovider.getauthentication(jwt);  //获取用户认证信息
    securitycontextholder.getcontext().setauthentication(authentication);   //将用户保存到securitycontext
   }
   filterchain.dofilter(servletrequest, servletresponse);
  }catch (expiredjwtexception e){          //jwt失效
   log.info("security exception for user {} - {}",
     e.getclaims().getsubject(), e.getmessage());
   log.trace("security exception trace: {}", e);
   ((httpservletresponse) servletresponse).setstatus(httpservletresponse.sc_unauthorized);
  }
 }
 private string resolvetoken(httpservletrequest request){
  string bearertoken = request.getheader(websecurityconfig.authorization_header);   //从http头部获取token
  if (stringutils.hastext(bearertoken) && bearertoken.startswith("bearer ")){
   return bearertoken.substring(7, bearertoken.length());        //返回token字符串,去除bearer
  }
  string jwt = request.getparameter(websecurityconfig.authorization_token);    //从请求参数中获取token
  if (stringutils.hastext(jwt)) {
   return jwt;
  }
  return null;
 }
}

编写 logincontroller,用户通过用户名、密码访问 /auth/login,通过 logindto 对象接收,创建一个 authentication 对象,代码中为 usernamepasswordauthenticationtoken,判断对象是否存在,通过 authenticationmanager 的 authenticate 方法对认证对象进行验证,authenticationmanager 的实现类 providermanager 会通过 authentionprovider(认证处理) 进行验证,默认 providermanager 调用 daoauthenticationprovider 进行认证处理,daoauthenticationprovider 中会通过 userdetailsservice(认证信息来源) 获取 userdetails ,若认证成功则返回一个包含权限的 authention,然后通过 securitycontextholder.getcontext().setauthentication() 设置到 securitycontext 中,根据 authentication 生成 token,并返回给用户

@restcontroller
public class logincontroller {
 @autowired
 private userrepository userrepository;
 @autowired
 private authenticationmanager authenticationmanager;
 @autowired
 private jwttokenutils jwttokenutils;
 @requestmapping(value = "/auth/login",method = requestmethod.post)
 public string login(@valid logindto logindto, httpservletresponse httpresponse) throws exception{
  //通过用户名和密码创建一个 authentication 认证对象,实现类为 usernamepasswordauthenticationtoken
  usernamepasswordauthenticationtoken authenticationtoken = new usernamepasswordauthenticationtoken(logindto.getusername(),logindto.getpassword());
  //如果认证对象不为空
  if (objects.nonnull(authenticationtoken)){
   userrepository.findbyname(authenticationtoken.getprincipal().tostring())
     .orelsethrow(()->new exception("用户不存在"));
  }
  try {
   //通过 authenticationmanager(默认实现为providermanager)的authenticate方法验证 authentication 对象
   authentication authentication = authenticationmanager.authenticate(authenticationtoken);
   //将 authentication 绑定到 securitycontext
   securitycontextholder.getcontext().setauthentication(authentication);
   //生成token
   string token = jwttokenutils.createtoken(authentication,false);
   //将token写入到http头部
   httpresponse.addheader(websecurityconfig.authorization_header,"bearer "+token);
   return "bearer "+token;
  }catch (badcredentialsexception authentication){
   throw new exception("密码错误");
  }
 }
}

编写 security 配置类,继承 websecurityconfigureradapter,重写 configure 方法

@configuration
@enablewebsecurity
@enableglobalmethodsecurity(prepostenabled = true)
public class websecurityconfig extends websecurityconfigureradapter {
 public static final string authorization_header = "authorization";
 public static final string authorization_token = "access_token";
 @autowired
 private userdetailsservice userdetailsservice;
 @override
 protected void configure(authenticationmanagerbuilder auth) throws exception {
  auth
    //自定义获取用户信息
    .userdetailsservice(userdetailsservice)
    //设置密码加密
    .passwordencoder(passwordencoder());
 }
 @override
 protected void configure(httpsecurity http) throws exception {
  //配置请求访问策略
  http
    //关闭csrf、cors
    .cors().disable()
    .csrf().disable()
    //由于使用token,所以不需要session
    .sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.stateless)
    .and()
    //验证http请求
    .authorizerequests()
    //允许所有用户访问首页 与 登录
    .antmatchers("/","/auth/login").permitall()
    //其它任何请求都要经过认证通过
    .anyrequest().authenticated()
    //用户页面需要用户权限
    .antmatchers("/userpage").hasanyrole("user")
    .and()
    //设置登出
    .logout().permitall();
  //添加jwt filter 在
  http
    .addfilterbefore(genericfilterbean(), usernamepasswordauthenticationfilter.class);
 }
 @bean
 public passwordencoder passwordencoder() {
  return new bcryptpasswordencoder();
 }
 @bean
 public genericfilterbean genericfilterbean() {
  return new jwtauthenticationtokenfilter();
 }
}

编写用于测试的controller

@restcontroller
public class usercontroller {
 @postmapping("/login")
 public string login() {
  return "login";
 }
 @getmapping("/")
 public string index() {
  return "hello";
 }
 @getmapping("/userpage")
 public string httpapi() {
  system.out.println(securitycontextholder.getcontext().getauthentication().getprincipal());
  return "userpage";
 }
 @getmapping("/adminpage")
 public string httpsuite() {
  return "userpage";
 }
}

案例源码下载  ()

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对移动技术网的支持。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网