Spring Security结合JWT的方法教程
概述
众所周知使用 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"; } }
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
推荐阅读
-
Spring Security结合JWT的方法教程
-
Spring-boot结合Shrio实现JWT的方法
-
使用Spring Security控制会话的方法
-
Spring Boot + Kotlin整合MyBatis的方法教程
-
如何使用Spring Security手动验证用户的方法示例
-
Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器
-
SpringBoot与spring security的结合的示例
-
自定义Spring Security的身份验证失败处理方法
-
php JWT在web端中的使用方法教程
-
基于Spring Security的Oauth2授权实现方法