shiro,基于springboot,基于前后端分离,从登录认证到鉴权,从入门到放弃
这个demo是基于springboot项目的。
名词介绍:
shiro
shiro 主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 subject、 securitymanager、 realms,公共部分 shiro 都已经为我们封装好了,我们只需要按照一定的规则去编写响应的代码即可…
subject 即表示主体,将用户的概念理解为当前操作的主体,因为它即可以是一个通过浏览器请求的用户,也可能是一个运行的程序,外部应用与 subject 进行交互,记录当前操作用户。subject 代表了当前用户的安全操作,securitymanager 则管理所有用户的安全操作。
securitymanager 即安全管理器,对所有的 subject 进行安全管理,并通过它来提供安全管理的各种服务(认证、授权等)
realm 充当了应用与数据安全间的 桥梁 或 连接器。当对用户执行认证(登录)和授权(访问控制)验证时,shiro 会从应用配置的 realm 中查找用户及其权限信息。
1.导入shiro依赖
<dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-spring</artifactid> <version>1.4.0</version> </dependency> <dependency> <groupid>org.crazycake</groupid> <artifactid>shiro-redis</artifactid> <version>2.8.24</version> </dependency>
shiro-redis为什么要导入这个包呢?将用户信息交给redis管理。
2.shiro配置类
package com.test.cbd.shiro; import org.apache.shiro.authc.credential.hashedcredentialsmatcher; import org.apache.shiro.mgt.securitymanager; import org.apache.shiro.session.mgt.sessionmanager; import org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor; import org.apache.shiro.spring.web.shirofilterfactorybean; import org.apache.shiro.web.mgt.defaultwebsecuritymanager; import org.crazycake.shiro.rediscachemanager; import org.crazycake.shiro.redismanager; import org.crazycake.shiro.redissessiondao; import org.springframework.aop.framework.autoproxy.defaultadvisorautoproxycreator; import org.springframework.beans.factory.annotation.value; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.servlet.handlerexceptionresolver; import java.util.linkedhashmap; import java.util.map; @configuration public class shiroconfig { @value("${spring.redis.shiro.host}") private string host; @value("${spring.redis.shiro.port}") private int port; @value("${spring.redis.shiro.timeout}") private int timeout; @value("${spring.redis.shiro.password}") private string password; @bean public shirofilterfactorybean shirfilter(securitymanager securitymanager) { system.out.println("shiroconfiguration.shirfilter()"); shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); shirofilterfactorybean.setsecuritymanager(securitymanager); map<string, string> filterchaindefinitionmap = new linkedhashmap<string, string>(); //注意过滤器配置顺序 不能颠倒 //配置退出 过滤器,其中的具体的退出代码shiro已经替我们实现了,登出后跳转配置的loginurl filterchaindefinitionmap.put("/logout", "logout"); // 配置不会被拦截的链接 顺序判断,在 shiroconfiguration 中的 shirofilter 处配置了 /ajaxlogin=anon,意味着可以不需要认证也可以访问 filterchaindefinitionmap.put("/static/**", "anon"); filterchaindefinitionmap.put("/*.html", "anon"); filterchaindefinitionmap.put("/ajaxlogin", "anon"); filterchaindefinitionmap.put("/login", "anon"); filterchaindefinitionmap.put("/**", "authc"); //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 shirofilterfactorybean.setloginurl("/unauth"); // 登录成功后要跳转的链接 // shirofilterfactorybean.setsuccessurl("/index"); //未授权界面; // shirofilterfactorybean.setunauthorizedurl("/403"); shirofilterfactorybean.setfilterchaindefinitionmap(filterchaindefinitionmap); return shirofilterfactorybean; } /** * 凭证匹配器 * (由于我们的密码校验交给shiro的simpleauthenticationinfo进行处理了 * ) * * @return */ @bean public hashedcredentialsmatcher hashedcredentialsmatcher() { hashedcredentialsmatcher hashedcredentialsmatcher = new hashedcredentialsmatcher(); hashedcredentialsmatcher.sethashalgorithmname("md5");//散列算法:这里使用md5算法; hashedcredentialsmatcher.sethashiterations(1024);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedcredentialsmatcher; } @bean public myshirorealm myshirorealm() { myshirorealm myshirorealm = new myshirorealm(); myshirorealm.setcredentialsmatcher(hashedcredentialsmatcher()); return myshirorealm; } @bean public securitymanager securitymanager() { defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager(); securitymanager.setrealm(myshirorealm()); // 自定义session管理 使用redis securitymanager.setsessionmanager(sessionmanager()); // 自定义缓存实现 使用redis securitymanager.setcachemanager(cachemanager()); return securitymanager; } //自定义sessionmanager @bean public sessionmanager sessionmanager() { mysessionmanager mysessionmanager = new mysessionmanager(); mysessionmanager.setsessiondao(redissessiondao()); return mysessionmanager; } /** * 配置shiro redismanager * <p> * 使用的是shiro-redis开源插件 * * @return */ public redismanager redismanager() { redismanager redismanager = new redismanager(); redismanager.sethost(host); redismanager.setport(port); redismanager.setexpire(1800);// 配置缓存过期时间 redismanager.settimeout(timeout); redismanager.setpassword(password); return redismanager; } /** * cachemanager 缓存 redis实现 * <p> * 使用的是shiro-redis开源插件 * * @return */ @bean public rediscachemanager cachemanager() { rediscachemanager rediscachemanager = new rediscachemanager(); rediscachemanager.setredismanager(redismanager()); return rediscachemanager; } /** * redissessiondao shiro sessiondao层的实现 通过redis * <p> * 使用的是shiro-redis开源插件 */ @bean public redissessiondao redissessiondao() { redissessiondao redissessiondao = new redissessiondao(); redissessiondao.setredismanager(redismanager()); return redissessiondao; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * * @param securitymanager * @return */ @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(securitymanager securitymanager) { authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return authorizationattributesourceadvisor; } /** * 注册全局异常处理 * @return */ @bean(name = "exceptionhandler") public handlerexceptionresolver handlerexceptionresolver() { return new myexceptionhandler(); } //自动创建代理,没有这个鉴权可能会出错 @bean public defaultadvisorautoproxycreator getdefaultadvisorautoproxycreator() { defaultadvisorautoproxycreator autoproxycreator = new defaultadvisorautoproxycreator(); autoproxycreator.setproxytargetclass(true); return autoproxycreator; } }
3.安全认证和权限验证的核心,自定义realm
package com.test.cbd.shiro; import com.google.gson.jsonobject; import com.test.cbd.service.userservice; import com.test.cbd.vo.syspermission; import com.test.cbd.vo.sysrole; import com.test.cbd.vo.userinfo; import org.apache.commons.beanutils.beanutils; import org.apache.shiro.securityutils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.authorizationinfo; import org.apache.shiro.authz.simpleauthorizationinfo; import org.apache.shiro.realm.authorizingrealm; import org.apache.shiro.session.session; import org.apache.shiro.subject.principalcollection; import org.apache.shiro.subject.subject; import org.apache.shiro.util.bytesource; import springfox.documentation.spring.web.json.json; import javax.annotation.resource; import java.util.hashset; import java.util.set; public class myshirorealm extends authorizingrealm { @resource private userservice userinfoservice; @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals){ // // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo(); session session = securityutils.getsubject().getsession(); userinfo user = (userinfo) session.getattribute("user_session"); // 用户的角色集合 set<string> roles = new hashset<>(); roles.add(user.getrolelist().get(0).getrole()); authorizationinfo.setroles(roles); return authorizationinfo; } /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/ @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { // system.out.println("myshirorealm.dogetauthenticationinfo()"); //获取用户的输入的账号. string username = (string) token.getprincipal(); // system.out.println(token.getcredentials()); //通过username从数据库中查找 user对象,如果找到,没找到. //实际项目中,这里可以根据实际情况做缓存,如果不做,shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 userinfo userinfo = userinfoservice.findbyusername(username); // subject subject = securityutils.getsubject(); //session session = subject.getsession(); //session.setattribute("role",userinfo.getrolelist()); // system.out.println("----->>userinfo="+userinfo); if (userinfo == null) { return null; } if (userinfo.getstate() == 1) { //账户冻结 throw new lockedaccountexception(); } string credentials = userinfo.getpassword(); system.out.println("credentials="+credentials); bytesource credentialssalt = bytesource.util.bytes(username); simpleauthenticationinfo authenticationinfo = new simpleauthenticationinfo( userinfo.getusername(), //用户名 credentials, //密码 credentialssalt, getname() //realm name ); session session = securityutils.getsubject().getsession(); session.setattribute("user_session", userinfo); return authenticationinfo; } }
4.全局异常处理器
package com.test.cbd.shiro; import com.alibaba.fastjson.support.spring.fastjsonjsonview; import org.apache.shiro.authz.unauthenticatedexception; import org.apache.shiro.authz.unauthorizedexception; import org.springframework.web.servlet.handlerexceptionresolver; import org.springframework.web.servlet.modelandview; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import java.util.hashmap; import java.util.map; /** * created by administrator on 2017/12/11. * 全局异常处理 */ public class myexceptionhandler implements handlerexceptionresolver { public modelandview resolveexception(httpservletrequest httpservletrequest,
httpservletresponse httpservletresponse, object o, exception ex) { modelandview mv = new modelandview(); fastjsonjsonview view = new fastjsonjsonview(); map<string, object> attributes = new hashmap<string, object>(); if (ex instanceof unauthenticatedexception) { attributes.put("code", "1000001"); attributes.put("msg", "token错误"); } else if (ex instanceof unauthorizedexception) { attributes.put("code", "1000002"); attributes.put("msg", "用户无权限"); } else { attributes.put("code", "1000003"); attributes.put("msg", ex.getmessage()); } view.setattributesmap(attributes); mv.setview(view); return mv; } }
5.因为现在的项目大多都是前后端分离的,所以我们需要实现自己的session管理
package com.test.cbd.shiro; import org.apache.shiro.web.servlet.shirohttpservletrequest; import org.apache.shiro.web.session.mgt.defaultwebsessionmanager; import org.apache.shiro.web.util.webutils; import org.springframework.util.stringutils; import javax.servlet.servletrequest; import javax.servlet.servletresponse; import java.io.serializable; public class mysessionmanager extends defaultwebsessionmanager { private static final string token = "token"; private static final string referenced_session_id_source = "stateless request"; public mysessionmanager() { super(); } @override protected serializable getsessionid(servletrequest request, servletresponse response) { string id = webutils.tohttp(request).getheader(token); //如果请求头中有 token 则其值为sessionid if (!stringutils.isempty(id)) { request.setattribute(shirohttpservletrequest.referenced_session_id_source, referenced_session_id_source); request.setattribute(shirohttpservletrequest.referenced_session_id, id); request.setattribute(shirohttpservletrequest.referenced_session_id_is_valid, boolean.true); return id; } else { //否则按默认规则从cookie取sessionid return super.getsessionid(request, response); } } }
6.控制器
package com.test.cbd.shiro; import com.alibaba.fastjson.jsonobject; import com.test.cbd.vo.userinfo; import io.swagger.annotations.api; import lombok.extern.slf4j.slf4j; import org.apache.shiro.securityutils; import org.apache.shiro.authc.authenticationexception; import org.apache.shiro.authc.incorrectcredentialsexception; import org.apache.shiro.authc.lockedaccountexception; import org.apache.shiro.authc.usernamepasswordtoken; import org.apache.shiro.authz.annotation.requiresroles; import org.apache.shiro.crypto.hash.simplehash; import org.apache.shiro.session.session; import org.apache.shiro.subject.subject; import org.apache.shiro.util.bytesource; import org.springframework.web.bind.annotation.*; import javax.servlet.http.httpservletrequest; import java.net.inetaddress; @slf4j @api(value="shiro测试",description="shiro测试") @restcontroller @requestmapping("/") public class shirologincontroller { /** * 登录测试 * @param userinfo * @return */ @requestmapping(value = "/ajaxlogin", method = requestmethod.post) @responsebody public string ajaxlogin(userinfo userinfo) { jsonobject jsonobject = new jsonobject(); usernamepasswordtoken token = new usernamepasswordtoken(userinfo.getusername(), userinfo.getpassword()); subject subject = securityutils.getsubject(); try { subject.login(token); jsonobject.put("token", subject.getsession().getid()); jsonobject.put("msg", "登录成功"); } catch (incorrectcredentialsexception e) { jsonobject.put("msg", "密码错误"); } catch (lockedaccountexception e) { jsonobject.put("msg", "登录失败,该用户已被冻结"); } catch (authenticationexception e) { jsonobject.put("msg", "该用户不存在"); } catch (exception e) { e.printstacktrace(); } return jsonobject.tostring(); } /** * 鉴权测试 * @param userinfo * @return */ @requestmapping(value = "/check", method = requestmethod.get) @responsebody @requiresroles("guest") public string check() { jsonobject jsonobject = new jsonobject(); jsonobject.put("msg", "鉴权测试"); return jsonobject.tostring(); } }
常用注解
@requiresguest 代表无需认证即可访问,同理的就是 /path=anon
@requiresauthentication 需要认证,只要登录成功后就允许你操作
@requirespermissions 需要特定的权限,没有则抛出 authorizationexception
@requiresroles 需要特定的橘色,没有则抛出 authorizationexception
7.以上就是shiro登陆和鉴权的主要配置和类,下面补充一下其他信息。
①application.properties中shiro-redis相关配置:
spring.redis.shiro.host=127.0.0.1 spring.redis.shiro.port=6379 spring.redis.shiro.timeout=5000 spring.redis.shiro.password=123456
②因为数据是模拟的,所以在登陆认证的时候,并没有通过数据库查用户信息,可以通过以下方式模拟加密后的密码:
/** * 生成测试用的md5加密的密码 * @param args */ public static void main(string[] args) { string hashalgorithmname = "md5"; string credentials = "123456";//密码 int hashiterations = 1024; bytesource credentialssalt = bytesource.util.bytes("root");//账号 string obj = new simplehash(hashalgorithmname, credentials, credentialssalt, hashiterations).tohex(); system.out.println(obj); }
上面obj的结果是b1ba853525d0f30afe59d2d005aad96c
③登陆认证的findbyusername方法,模拟到数据库查询用户信息,实际是自己构造的数据,偷偷懒。
public userinfo findbyusername(string username){ sysrole admin = sysrole.builder().role("admin").build(); list<syspermission> list=new arraylist<syspermission>(); syspermission syspermission=new syspermission("read"); syspermission syspermission1=new syspermission("write"); list.add(syspermission); list.add(syspermission1); admin.setpermissions(list); userinfo root = userinfo.builder().username("root").password("b1ba853525d0f30afe59d2d005aad96c").credentialssalt("123").state(0).build(); list<sysrole> rolelist=new arraylist<sysrole>(); rolelist.add(admin); root.setrolelist(rolelist); return root; }
8.结果演示
输入正确的账号密码时,返回信息如下:
故意输错密码时,返回信息如下:
鉴权时,如果该用户没有角色: