简介
现在主流的安全框架分别为shiro和spring security。关于两者之间的优缺点不是本文的重点,有兴趣的可以在网上搜搜,各种文章也都分析的很清楚。那么简单来说,shiro是一个强大易用的java安全框架,提供了认证、授权、加密和会话管理等功能。(不一定要建立所谓的五张表,我们要做到控制自如的使用)
目的
通过集成shiro,jwt我们要实现:用户登录的校验;登录成功后返回成功并携带具有身份信息的token以便后续调用接口的时候做认证;对项目的接口进行权限的限定等。
需要的jar
本文使用的gradel作为jar包管理工具,maven也是使用相同的jar
//shiro的jar implementation 'org.apache.shiro:shiro-spring:1.7.1' //jwt的jar implementation 'com.auth0:java-jwt:3.15.0' implementation 'com.alibaba:fastjson:1.2.76' compileonly 'org.projectlombok:lombok' annotationprocessor 'org.projectlombok:lombok'
集成过程
1.配置shiro
@configuration public class shiroconfig { /* * 解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring * aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射 */ @bean public static defaultadvisorautoproxycreator creator() { defaultadvisorautoproxycreator creator = new defaultadvisorautoproxycreator(); creator.setproxytargetclass(true); return creator; } /** * enable shiro aop annotation support. --<1> * * @param securitymanager security manager * @return authorizationattributesourceadvisor */ @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(securitymanager securitymanager) { authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return authorizationattributesourceadvisor; } /** * use for login password matcher --<2> * * @return hashedcredentialsmatcher */ @bean("hashedcredentialsmatcher") public hashedcredentialsmatcher hashedcredentialsmatcher() { hashedcredentialsmatcher matcher = new hashedcredentialsmatcher(); // set name of hash matcher.sethashalgorithmname("sha-256"); // storage format is hexadecimal matcher.setstoredcredentialshexencoded(true); return matcher; } /** * realm for login --<3> * * @param matcher password matcher * @return passwordrealm */ @bean public loginrealm loginrealm(@qualifier("hashedcredentialsmatcher") hashedcredentialsmatcher matcher) { loginrealm loginrealm = new loginrealm(login); loginrealm.setcredentialsmatcher(matcher); return loginrealm; } /** * jwtreal, use for token validation --<4> * * @return jwtrealm */ @bean public jwtrealm jwtrealm() { return new jwtrealm(jwt); } // --<5> @bean public ourmodularrealmauthenticator usermodularrealmauthenticator() { // rewrite modularrealmauthenticator dataauthmodularrealmauthenticator modularrealmauthenticator = new dataauthmodularrealmauthenticator(); modularrealmauthenticator.setauthenticationstrategy(new atleastonesuccessfulstrategy()); return modularrealmauthenticator; } // --<6> @bean(name = "securitymanager") public securitymanager securitymanager( @qualifier("usermodularrealmauthenticator") ourmodularrealmauthenticatormodular, @qualifier("jwtrealm") jwtrealm jwtrealm, @qualifier("loginrealm") loginrealm loginrealm ) { defaultwebsecuritymanager manager = new defaultwebsecuritymanager(); // set realm manager.setauthenticator(modular); // set to use own realm list<realm> realms = new arraylist<>(); realms.add(loginrealm); realms.add(jwtrealm); manager.setrealms(realms); // close shiro's built-in session defaultsubjectdao subjectdao = new defaultsubjectdao(); defaultsessionstorageevaluator defaultsessionstorageevaluator = new defaultsessionstorageevaluator(); defaultsessionstorageevaluator.setsessionstorageenabled(false); subjectdao.setsessionstorageevaluator(defaultsessionstorageevaluator); manager.setsubjectdao(subjectdao); return manager; } // --<7> @bean(name = "shirofilter") public shirofilterfactorybean shirofilter(@qualifier("securitymanager") securitymanager securitymanager) { shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); shirofilterfactorybean.setsecuritymanager(securitymanager); map<string, filter> filter = new linkedhashmap<>(1); filter.put("jwt", new jwtfilter()); shirofilterfactorybean.setfilters(filter); map<string, string> filtermap = new hashmap<>(); filtermap.put("/login/**", "anon"); filtermap.put("/v1/**", "jwt"); shirofilterfactorybean.setfilterchaindefinitionmap(filtermap); return shirofilterfactorybean; } }
- 开启shiro注解支持,具体原理请参考springboot中shiro使用自定义注解屏蔽接口鉴权实现
- 配置shiro登录验证的密码加密方式:shiro 提供了用于加密密码和验证密码服务的 credentialsmatcher 接口,hashedcredentialsmatcher 正是 credentialsmatcher 的一个实现类。
- loginrealm:自定义的realm,用于处理用户登录验证的realm,在shiro中验证及授权等信息会在realm中配置,详细解释请参考shiro简介
- jwtrealm:自定义的realm,用户在登录后访问服务时做token的校验,用户权限的校验等。
- 配置dataauthmodularrealmauthenticator:是在项目中存在多个realm时,根据项目的认证策略可以选择匹配需要的realm。
- securitymanager:shiro的核心组件,管理着认证、授权、会话管理等,在这里我把所有的自定义的realm等资源加入到securitymanager中
- shiro的过滤器:定制项目的path过滤规则,并将我们自定义的filter加入到shiro中的shirofilterfactorybean中
2.创建自定义realm
2.1 loginrealm用于处理用户登录
public class loginrealm extends authorizingrealm { public loginrealm(string name) { setname(name); } // 获取user相关信息的service类 @autowired private userloginservice userloginservice; // supports方法必须重写,这是shiro处理流程中的一部分,他会通过此方法判断realm是否匹配的正确 @override public boolean supports(authenticationtoken token) { return token instanceof logindataautotoken; } @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals) { return null; } @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken auth) throws authenticationexception { logindataautotoken token = (logindataautotoken) auth; servicelog.info(token.getusername() + "password auth start..."); user user = userloginservice.selectuserbyname(token.getusername()); if (user == null) throw new unknownaccountexception(); object credentials = user.getpassword(); // save username and role to attribute servletutils.usernameroleto.accept(user.getusername(), (int) user.getusertype()); return new simpleauthenticationinfo(user, credentials, super.getname()); } }
2.2 jwtrealm用于在登录之后,用户的token是否正确以及给当前用户授权等
public class jwtrealm extends authorizingrealm { public jwtrealm(string name) { setname(name); } @override public boolean supports(authenticationtoken token) { return token instanceof jwtdataautotoken; } // 给当前用户授权,只有在访问的接口上配置了shiro的权限相关的注解的时候才会进入此方法 @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals) { simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo(); userenum.type userenum = enumvalue.datavalueof( userenum.type.class, servletutils.usernamerolefrom.get().getuserrole() ); set<string> roles = new hashset<>(); roles.add(userenum.getdesc()); // 授权角色如果有其他的权限则都已此类的方式授权 authorizationinfo.setroles(roles); return authorizationinfo; } // 验证此次request携带的token是否正确,如果正确解析当前token,并存入上下文中 @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken auth) throws authenticationexception { // verify token string token = (string) auth.getcredentials(); tokenutils.verify(token); tuplenamerole tuplenamerole = tokenutils.tokendecode(token); servletutils.usernameroleto.accept(tuplenamerole.getusername(), tuplenamerole.getuserrole()); return new simpleauthenticationinfo(token, token, ((jwtdataautotoken) auth).getname()); } }
2.3 ourmodularrealmauthenticator用于匹配的相应的realm
public class dataauthmodularrealmauthenticator extends modularrealmauthenticator { @override protected authenticationinfo doauthenticate(authenticationtoken authenticationtoken) throws authenticationexception { assertrealmsconfigured(); dataautotoken dataautotoken = (dataautotoken) authenticationtoken; realm realm = getrealm(dataautotoken); return dosinglerealmauthentication(realm, authenticationtoken); } private realm getrealm(dataautotoken dataautotoken) { for (realm realm : getrealms()) { // 根据定义的realm的name和dataautotoken的name匹配相应的realm if (realm.getname().contains(dataautotoken.getname())) { return realm; } } return null; } }
2.4 dataautotoken及实现类
dataauthmodularrealmauthenticator的dosinglerealmauthentication(realm, authenticationtoken)做检验的时候需要两个参数,一个是realm另一个是我们定义的储存验证信息的authenticationtoken或者它的实现类。
dataautotoken:
public interface dataautotoken { string getname(); }
logindataautotoken :
public class logindataautotoken extends usernamepasswordtoken implements dataauthtoken { public logindataauthtoken(final string username, final string password) { super(username, password); } @override public string getname() { return login; } }
jwtdataautotoken:
public class jwtdataautotoken implements authenticationtoken, dataauthtoken { private final string token; public jwtdataauthtoken(string token) { this.token = token; } @override public object getprincipal() { return token; } @override public object getcredentials() { return token; } @override public string getname() { return jwt; } }
2.5 jwtfilter处理在shiro配置的自定义的filter
此类用于处理不在登录下必须携带发行的token访问接口,如果token存在,则使用shiro subject做token的和访问权限的校验。
public class jwtfilter extends basichttpauthenticationfilter { private final biconsumer<servletresponse, errormessage> writeresponse = (response, message) -> utils.renderstring.accept( (httpservletresponse) response, json.tojsonstring(responseresult.fail(message), serializerfeature.writemapnullvalue) ); /** * @param request servletrequest * @param response servletresponse * @param mappedvalue mappedvalue * @return 是否成功 */ @override protected boolean isaccessallowed(servletrequest request, servletresponse response, object mappedvalue) { httpservletrequest httpservletrequest = (httpservletrequest) request; //input request to request log file requestlog.info( "path:{}, method:{}", httpservletrequest.getservletpath(), httpservletrequest.getmethod() ); string token = httpservletrequest.getheader(constant.token); if (token != null) { return executelogin(request, response); } else { writeresponse.accept(response, errormessage.token_not_exist); return false; } } /** * execute login */ @override protected boolean executelogin(servletrequest request, servletresponse response) { httpservletrequest httpservletrequest = (httpservletrequest) request; string token = httpservletrequest.getheader(constant.token); try { jwtdataauthtoken jwttoken = new jwtdataauthtoken(token); // validate user permission getsubject(request, response).login(jwttoken); return true; } catch (authenticationexception e) { throwable throwable = e.getcause(); if (throwable instanceof tokenexpiredexception) { writeresponse.accept(response, errormessage.token_has_expired); } else { writeresponse.accept(response, errormessage.token_invalid); } } return false; } /** * support across domains */ @override protected boolean prehandle(servletrequest request, servletresponse response) throws exception { httpservletrequest httpservletrequest = (httpservletrequest) request; httpservletresponse httpservletresponse = (httpservletresponse) response; httpservletresponse.setheader("access-control-allow-origin", httpservletrequest.getheader("origin")); httpservletresponse.setheader("access-control-allow-methods", "get,post,options,put,delete"); httpservletresponse.setheader("access-control-allow-headers", httpservletrequest.getheader("access-control-request-headers")); if (httpservletrequest.getmethod().equals(requestmethod.options.name())) { httpservletresponse.setstatus(httpstatus.ok.value()); return false; } return super.prehandle(request, response); }
2.6 controller层登录和其他接口
@restcontroller public class authcontroller { @autowired private userservice userservice; @postmapping("/login") public responseresult<string> login(@requestbody userreqdto userreqdto) { userservice.login(userloginreqdto.getusername(), userreqdto.getpassword()); return responseresult.success(); } // shiro角色注解,admin才可以访问此接口 @requiresroles("admin") @postmapping("/v1/user") public responseresult<string> adduser(@requestbody useraddreqdto useraddreqdto) { userservice.add(useraddreqdto); return responseresult.success(); } @postmapping("/v1/token/verify") public responseresult<string> verify() { return responseresult.success(false); } @postmapping("/v1/token/refresh") public responseresult<string> refresh() { return responseresult.success(); } }
2.7 service层
@service public class userserviceimpl implements userservice { @override public void login(string username, string password) { // use shiro to verify the username and password subject subject = securityutils.getsubject(); logindataautotoken token = new logindataautotoken(username, password); subject.login(token); } @transactional @override public void add(useraddreqdto dto) { user user = getuserbyname.apply(dto.getusername()); if (user != null) { throw new dataauthexception(errormessage.user_already_exists); } else { user newuser = new user(); // 设置user的信息 post(newuser); // insert user to database } }
2.8 jwt工具类
public final class tokenutils { private tokenutils() { } /** * @param username username * @param role user role * @return the encrypted token */ public static string createtoken(string username, int role) { date date = new date(system.currenttimemillis() + constant.token_expire_time); algorithm algorithm = algorithm.hmac256(username); return jwt.create() .withclaim(constant.user_name, username) .withclaim(constant.user_role, role) .withexpiresat(date) .sign(algorithm); } /** * @param username username * @param role user role * @return the encrypted token */ public static string refreshtoken(string username, int role) { return createtoken(username, role); } /** * refresh token and add to header */ public static void refreshtoken() { tuplenamerole tuplenamerole = servletutils.usernamerolefrom.get(); servletutils.addheader.accept( constant.token, createtoken(tuplenamerole.getusername(), tuplenamerole.getuserrole()) ); } /** * verify token * * @param token jwttoken */ public static void verify(string token) { try { tuplenamerole tuplenamerole = tokendecode(token); algorithm algorithm = algorithm.hmac256(tuplenamerole.getusername()); jwtverifier verifier = jwt.require(algorithm) .withclaim(constant.user_name, tuplenamerole.getusername()) .withclaim(constant.user_role, tuplenamerole.getuserrole()) .build(); verifier.verify(token); } catch (jwtverificationexception e) { servicelog.error("token verify fail.", e); throw e; } } /** * @param token token * @return user name and role */ public static tuplenamerole tokendecode(string token) { try { decodedjwt jwt = jwt.decode(token); return new tuplenamerole( jwt.getclaim(constant.user_name).asstring(), jwt.getclaim(constant.user_role).asint() ); } catch (jwtdecodeexception e) { servicelog.error("token decode happen exception.", e); throw e; } } }
2.9 其他的一些工具类
servletutils:与spring context中有关的一些方法
public final class servletutils { private servletutils() { } private static final int scope = requestattributes.scope_request; private static final supplier<servletrequestattributes> servletrequestattributes = () -> (servletrequestattributes) requestcontextholder.getrequestattributes(); private static final supplier<httpservletrequest> request = () -> servletrequestattributes.get().getrequest(); private static final supplier<httpservletresponse> response = () -> servletrequestattributes.get().getresponse(); private static final consumer<string> saveusernametoattribute = (name) -> servletrequestattributes.get().setattribute(constant.user_name, name, scope); private static final supplier<string> usernamefromattribute = () -> (string) servletrequestattributes.get().getattribute(constant.user_name, scope); private static final consumer<integer> saveuserroletoattribute = (role) -> servletrequestattributes.get().setattribute(constant.user_role, role, scope); private static final supplier<integer> userrolefromattribute = () -> (integer) servletrequestattributes.get().getattribute(constant.user_role, scope); /** * get token form current request */ public static supplier<string> tokenfromrequest = () -> request.get().getheader(constant.token); /** * save current user name and role to attribute */ public static biconsumer<string, integer> usernameroleto = (name, role) -> { saveusernametoattribute.accept(name); saveuserroletoattribute.accept(role); }; /** * get user name and role from attribute */ public static supplier<tuplenamerole> usernamerolefrom = () -> new tuplenamerole(usernamefromattribute.get(), userrolefromattribute.get()); /** * add message to response header */ public static biconsumer<string, string> addheader = (key, value) -> response.get().addheader(key, value); }
utils:提供与shiro相同的密码加密方式、获取uuid、shiro的filter层出错不能使用全局异常处理时的返回信息定制等。
public final class utils { private utils() { } /** * use sha256 encrypt */ public static function<string, string> encryptpassword = (password) -> new sha256hash(password).tostring(); /** * get uuid */ public static supplier<string> uuid = () -> uuid.randomuuid().tostring().replace("-", ""); /** * writer message to response */ public static biconsumer<httpservletresponse, string> renderstring = (response, body) -> { response.setstatus(httpstatus.ok.value()); response.setcharacterencoding("utf-8"); response.setcontenttype("application/json;charset=utf-8"); try (printwriter writer = response.getwriter()) { writer.print(body); } catch (ioexception e) { servicelog.error("response error.", e); } }; }
2.10 返回结果定义
@data public class responseresult<t> implements serializable { private static final long serialversionuid = 1l; private final string code; @jsonfield(ordinal = 1) private final string msg; @jsonfield(ordinal = 2) private t data; private responseresult(string code, string msg) { this.code = code; this.msg = msg; log(); } private static <t> responseresult<t> create(string code, string msg) { return new responseresult<>(code, msg); } /** * no data returned successfully * * @return responseresult<string> */ public static <t> responseresult<t> success() { return success(true); } /** * no data returned successfully * * @param refreshtoken whether to refresh token * @return responseresult<string> */ public static <t> responseresult<t> success(boolean refreshtoken) { if (refreshtoken) tokenutils.refreshtoken(); return create(errormessage.success.code(), errormessage.success.msg()); } public static <t> responseresult<t> success(t data) { return success(data, true); } /** * data returned successfully * * @param data data * @param <t> t * @param refreshtoken whether to refresh token * @return responseresult<t> */ public static <t> responseresult<t> success(t data, boolean refreshtoken) { responseresult<t> responseresult = success(refreshtoken); responseresult.setdata(data); return responseresult; } /** * @param e dcexception * @return responseresult<string> */ public static responseresult<string> fail(dataauthexception e) { return create(e.getcode(), e.getmsg()); } /** * @param errormessage errormessage * @return responseresult<string> */ public static responseresult<string> fail(errormessage errormessage) { return create(errormessage.code(), errormessage.msg()); } /** * @param errormessage dcexception * @return responseresult<string> */ public static responseresult<string> fail(errormessage errormessage, object[] detailmessage) { return create(errormessage.code(), errormessage.msg() + arrays.tostring(detailmessage)); } // output the information returned private void log() { requestlog.info("code:{}, msg:{}", this.getcode(), this.getmsg()); } }
到此这篇关于springboot项目中集成shiro+jwt详解+完整实例的文章就介绍到这了,更多相关springboot 集成shiro jwt内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论