博主简介:原互联网大厂tencent员工,网安巨头Venustech员工,阿里云开发社区专家博主,微信公众号java基础笔记优质创作者,csdn优质创作博主,创业者,知识共享者,欢迎关注,点赞,收藏。
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。上个月写了一个在线教育的项目用到了shiro权限控制,这几天又复盘了一下,对其进行了深入探究,来总结一下。
下面所总结的有关shiro的代码已经传到我的github上,可以访问下面的链接:
GitHub - xujiankang6/shirotest
一、实现功能
1、完成了记住我功能
2、完成了密码加密功能
3、完成了根据shiro权限访问不同内容
4、完成了使用shiro的session进行保存
二、实现代码
1、引入shiro相关的依赖
我是前端使用了thymeleaf,所以需要引入下面shiro相关的依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.3.2</version></dependency><dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>2.0.0</version></dependency>
2、自定义密码验证器
(1)这个类实现了shiro的SimpleCredentialsMatcher接口来重新密码验证方法
(2)同时写了加密的方法encrypt(String data),拿到前台传过来的密码后,使用该方法加密后与数据库的拿到的密码进行比对,返回ture或者fasle。
(3)当然,前台注册时,保存数据库的密码也需要用同样的形式把密码加密后再保存。
//验证密码 查找到了1该用户 自定义密码验证器public class MyMatcher extends SimpleCredentialsMatcher {@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;String pwd = encrypt(String.valueOf(usernamePasswordToken.getPassword()));String mysqlpwd = (String) info.getCredentials();return this.equals(pwd, mysqlpwd);}//将传进来的密码进行加密的方法private String encrypt(String data){String sha384Hex=new Sha384Hash(data).toBase64();return sha384Hex;}}
3、自定义realm
当密码验证通过后,就到了我们的自定义realm,在我们自定义realm中实现了AuthorizingRealm接口,将其方法进行重写,将各种权限对用户进行授权,同时对用户身份进行验证,代码如下,每一行代码具体含义十分详细了。
//登录及权限验证public class MyRealm extends AuthorizingRealm {@AutowiredUserService us;//角色权限和对应权限添加//Authorization授权,将数据库中的角色和权限授权给输入的用户名@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//获取登录的用户名String phone = (String) principalCollection.getPrimaryPrincipal();//到数据库里查询要授权的内容User user = us.querybyname(phone);//记录用户的所有角色和权限SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();//权限信息for(Role r:user.getRoles()){//将所有的角色信息添加进来。simpleAuthorizationInfo.addRole(r.getRname());for(Permission p:r.getPermissions()){//将此次遍历到的角色的所有权限拿到,添加·进来simpleAuthorizationInfo.addStringPermission(p.getPname());}}return simpleAuthorizationInfo;}//用户身份验证@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {//从token获取用户名,从主体传过来的认证信息中获取//加这一步的目的是在post请求时会先进入认证然后再到请求。if(authenticationToken.getPrincipal()==null){return null;}//获取用户的登录信息,用户名String phone=authenticationToken.getPrincipal().toString();//根据service调用用户名,查找用户的全部信息//通过用户名到数据库获取凭证User user=us.querybyname(phone);if(user==null){//这里返回会报出对应异常return null;}else{//这里验证authenticationToken和simpleAuthenticationInfo的信息SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(phone,user.getUpwd().toString(),getName());return simpleAuthenticationInfo;}}
4、自定义记住我过滤器
该过滤器是为了实现记住我后,用户再次登陆不需要进行权限验证,就能到达首页,后面会介绍当不使用该过滤器的后果。
(1)该过滤器实现当用户通过isRemembered()登陆,没有通过isAuthenticated()登陆时拿到user的session信息,保证后面到达首页时候能拥有各种跟通过isAuthenticated()登陆时的session信息。
(2)过滤器完成了登陆条件的过滤,要么通过权限认证登陆成功,要么通过记住我登陆成功。
(3)在shiroconfig类中会进行shiro访问权限配置。
public class MyRememberFilter extends FormAuthenticationFilter {protected boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response, Object mappedValue){Subject subject=getSubject(request,response);if(!subject.isAuthenticated() && subject.isRemembered()){if(subject.getSession().getAttribute("user")==null &&subject.getPrincipal()!=null){subject.getSession().setAttribute("user",subject.getPrincipal());}}return subject.isAuthenticated() || subject.isRemembered();}}
5、实现shiroConfig配置类
(1)配置密码验证器
@Bean("credentialsMatcher")public CredentialsMatcher credentialsMatcher() {return new MyMatcher();}
(2)配置权限验证器
@Bean("myRealm")public MyRealm myRealm(@Qualifier("credentialsMatcher") CredentialsMatcher credentialsMatcher) {MyRealm myRealm = new MyRealm();//给权限验证器配置上自定义的密码验证器myRealm.setCredentialsMatcher(credentialsMatcher);return myRealm;}
(3)配置缓存验证器
@Beanpublic CacheManager cacheManager(){return new MemoryConstrainedCacheManager();}
(4)配置记住我Cookie对象参数,rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
public SimpleCookie rememberMeCookie() {// 这个参数是cookie的名称,对应前端的checkbox的name=rememberMeSimpleCookie simpleCookie = new SimpleCookie("rememberMe");// cookie生效时间为10秒simpleCookie.setMaxAge(10);return simpleCookie;}
(5)配置Cookie管理对象,rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
@Beanpublic CookieRememberMeManager rememberMeManager() {CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();cookieRememberMeManager.setCookie(rememberMeCookie());return cookieRememberMeManager;}
(6)注入自定义记住我过滤器
@Beanpublic MyRememberFilter MyRememberFilter(){return new MyRememberFilter();}
(7)配置securityManager安全管理器,主要起到一个桥梁作用。
@Bean("securityManager")public SecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm) {DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();//注入自定义myRealmdefaultWebSecurityManager.setRealm(myRealm);//注入自定义cacheManagerdefaultWebSecurityManager.setCacheManager(cacheManager());//注入记住我管理器defaultWebSecurityManager.setRememberMeManager(rememberMeManager());//注入自定义sessionManagerdefaultWebSecurityManager.setSessionManager(sessionManager());return defaultWebSecurityManager;}
(8)进行全局配置,Filter工厂。设置对应的过滤条件和跳转条件,有自定义的过滤器,有shiro认证成功后,失败后,退出后等跳转的页面,有静态页面等内容的权限范围。
@Bean("shiroFilterFactoryBean")public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {//shiro对象ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();bean.setSecurityManager(securityManager);bean.setLoginUrl("/login");bean.setSuccessUrl("/index");Map<String, Filter> filterMap=new LinkedHashMap<String,Filter>();filterMap.put("MyRememberFilter",MyRememberFilter());//MAPLinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String, String>();/*认证顺序是从上往下执行。*/linkedHashMap.put("/logout", "logout");//在这儿配置登出地址,不需要专门去写控制器。linkedHashMap.put("/static/**", "anon");//开启注册页面不需要权限linkedHashMap.put("/register", "anon");linkedHashMap.put("/saveregister", "anon");//验证phone唯一linkedHashMap.put("/solephone", "anon");//获取验证码linkedHashMap.put("/getcode", "anon");//验证码判断linkedHashMap.put("/comparecode", "anon");linkedHashMap.put("/websocket", "anon");//必须开启。linkedHashMap.put("/css/**", "anon");//不需要验证linkedHashMap.put("/js/**", "anon");//不需要验证//配置错误页面linkedHashMap.put("error", "anon");//不需要验证linkedHashMap.put("/img/**", "anon");//不需要验证linkedHashMap.put("/layui/**", "anon");//不需要验证linkedHashMap.put("/video/**", "anon");//不需要验证linkedHashMap.put("/bower_components/**", "anon");//不需要验证linkedHashMap.put("/plugins/**", "anon");//不需要验证linkedHashMap.put("/dist/**", "anon");//不需要验证linkedHashMap.put("/**", "user");//需要进行权限验证bean.setFilterChainDefinitionMap(linkedHashMap);return bean;}
(9)配置shiro的生命周期
@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}
(10)配置shiro注解是否生效
启动Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
@Beanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();sourceAdvisor.setSecurityManager(securityManager);return sourceAdvisor;}
(11)配置前台的shiro标签,使其能够使用
前台是使用shiro结合thymeleaf实现细粒度权限控制的。
@Beanpublic ShiroDialect shiroDialect() {return new ShiroDialect();}
6、用户登陆controller实现
(1)当用户第一次到达登陆页面时,通过get方式。
@RequestMapping(value = "/login", method = RequestMethod.GET)public String login(@Param("phone") String phone, @Param("upwd") String upwd) {return "login";}
(2)用户提交登陆信息时,通过post方法。
@RequestMapping(value = "/login", method = RequestMethod.POST)public ModelAndView login2(@Param("phone") String phone, @Param("upwd") String upwd) {ModelAndView m = new ModelAndView();//添加用户认证信息Subject subject = SecurityUtils.getSubject();UsernamePasswordToken uToken = new UsernamePasswordToken(phone, upwd);//实现记住我uToken.setRememberMe(true);try {//进行验证,报错返回首页,不报错到达成功页面。subject.login(uToken);} catch (UnknownAccountException e) {m.addObject("result", "用户不存在");m.setViewName("login");return m;} catch (IncorrectCredentialsException e) {m.addObject("result", "密码错误");m.setViewName("login");return m;}//将权限信息保存到session中User user = sd.querybyname(phone);List<Permission> permissions = new ArrayList<Permission>();for (Role role : user.getRoles()) {for (Permission permission : role.getPermissions()) {permissions.add(permission);}}Map<String, ArrayList<Permission>> map = new HashMap<String, ArrayList<Permission>>();for (Permission p : permissions) {String name = p.getPermission_group_name();if (!map.containsKey(name)) {ArrayList<Permission> mList = new ArrayList<Permission>();mList.add(p);map.put(name, mList);} else {ArrayList<Permission> pList = map.get(name);pList.add(p);map.put(name, pList);}}// 保存到shiro的session中一些信息Session session = subject.getSession();// 保存userinfo的基本信息int uid = user.getUid();UserInfo userInfo = sd.queryuserinfo(uid);//保存user信息session.setAttribute("user", user);//shiro权限验证成功后跳转的界面m.setViewName("index");return m;}
7、用户前端登录页面
(1)部分代码
<div class="login-box-body"><p class="login-box-msg">登录更精彩!!!</p><form action="login" method="post"><div class="form-group has-feedback"><input type="phone" class="form-control" placeholder="请输入手机号" name="phone"><span class="glyphicon glyphicon-envelope form-control-feedback"></span></div><div class="form-group has-feedback"><input type="password" class="form-control" placeholder="请输入密码" name="upwd"><span class="glyphicon glyphicon-lock form-control-feedback"></span></div><div class="row"><div class="col-xs-8"><div class="checkbox icheck"><label><input type="checkbox" name="rememberMe"> 记住账户</label></div></div><!-- /.col --><div class="col-xs-4"><button type="submit" class="btn btn-primary btn-block btn-flat">登录</button><p th:text="${result}" style="color: red"></p></div><!-- /.col --></div></form><!-- /.social-auth-links --><a href="#">忘记密码?</a> <a href="/register" class="text-center">注册</a></div><!-- /.login-box-body --></div>
(2)页面展示
8、前台使用shiro结合thymeleaf实现细粒度权限具体实现
<div class="description-block"><shiro:hasAnyRoles name="教员"><h5 class="description-header" th:text="${courses.size()}">35</h5></shiro:hasAnyRoles><shiro:hasAnyRoles name="管理员"><h5 class="description-header" >0</h5></shiro:hasAnyRoles><shiro:hasAnyRoles name="普通用户"><h5 class="description-header" >0</h5></shiro:hasAnyRoles><span class="description-text">课程数</span></div>
三、注意事项
1、记住我功能
(1)我们自定义记住我过滤器是为了在通过isRemember()登录时,能够得到和通过isAuthenticated()登录一样的信息,因为当我们通过isAuthenticated()登录时,我们获取了一下session信息,通过isRemember()登陆我们只拿到了账户密码信息,虽然能够登陆成功,但是由于缺少一些session信息,在到达首页时会报错,完成不了后续的操作。
(2)在shiroConfig中我们需要配置自定义的过滤器,并在该类的shiro全局配置中把该过滤器注入进来,让其功能实现。
Map<String, Filter> filterMap=new LinkedHashMap<String,Filter>();filterMap.put("MyRememberFilter",MyRememberFilter());
(3)我们需要把shiroConfig全局配置中的下面的权限改为user权限
linkedHashMap.put("/**", "authc");
如果我们使用authc权限,我们通过记住我登陆成功后,关闭浏览器,再打开还是需要输入账户密码,不能直接到达首页,没有实现效果,因为isRemember()的权限是user权限,比authc权限低,所以通过记住我登陆后,shiro会觉得不安全,让再次输入密码,只有改为user权限才能实现,下次打开浏览器,不需要输入密码,直接登陆成功。
(4)但是使用user权限又会带来新的问题,我们对一些敏感的信息,不想让用户通过user权限看到,这时需要对此种页面使用authc权限,当到达这样的页面时,我们需要账户密码重新登陆一下。
2、密码加密功能
(1)我们必须在MyMathcher中对拿到的前台的密码进行加密后再与数据库中的密码进行比对。
(2)在注册时,我们需要用相同的加密算法对用户注册的密码进行加密保存的数据库中,通过shiro验证时,拿加密后的数据库中的密码与前台用户登录时的密码加密后进行比对。这样才能够实现权限验证。
(3)我只是使用了一种简单的算法,大家可以用加盐的MD5,自行操作。
四、总结
以上是我对shiro的深入理解,希望对小伙伴能有帮助,更多精彩请关注:
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
/xander