欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Security结合JWT的方法教程

程序员文章站 2024-02-20 22:21:16
概述 众所周知使用 jwt 做权限验证,相比 session 的优点是,session 需要占用大量服务器内存,并且在多服务器时就会涉及到共享 session 问题,在手...

概述

众所周知使用 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";
 }
}

案例源码下载  (本地下载

总结

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