spring mvc环境之权限管理shiro(十六)
一.shiro相关概念
1)shiro介绍
shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。 shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。
应用场景:
- 在独立应用中使用
- 在web中使用
- 在spring框架中集成。
shiro 解决应用安全的四要素:
- 认证 - 用户身份识别,常被称为用户“登录”
- 授权 - 访问控制
- 密码加密 - 保护或隐藏数据防止被偷窥
- 会话管理 - 与用户相关的时间敏感的数据
2)shiro优势
易用: 相当于其他安全框架,shiro比较简单易用
广泛: 使用非常广泛,资料好找
灵活: 可以工作在很多工作环境,web,ejb,ioc等等
web支持: 对web的支持好,允许你基于应用 URL 和 Web 协议(如 REST)创建灵活的安全策略,同时还提供了一套控制页面输出的 JSP 标签库
支持:应用广泛,是 Apache 软件基金会成员
3)Apache Shiro 的三大核心组件
Shiro的体系结构具有3个主要概念:Subject(主体,用户),SecurityManager(安全管理器)和Realms(领域)。
4)shiro详细架构
-
Subject (org.apache.shiro.subject.Subject)
当前与软件进行交互的实体(用户,第三方服务,计划任务等)的特定于安全性的“视图”。
-
SecurityManager (org.apache.shiro.mgt.SecurityManager)
SecurityManager是Shiro架构的核心。 它主要是一个“伞”对象,用于协调其托管组件以确保它们能够顺利协作。 它还管理Shiro对每个应用程序用户的视图,因此它知道如何对每个用户执行安全操作。
-
Authenticator (org.apache.shiro.authc.Authenticator)
Authenticator是负责执行用户的身份验证(登录)并对其作出反应的组件。 当用户尝试登录时,该逻辑由身份验证器执行。 身份验证器知道如何与存储相关用户/帐户信息的一个或多个领域协调。 从这些领域获得的数据用于验证用户的身份,以确保用户确实是他们所说的真实身份。
-
Authentication Strategy (org.apache.shiro.authc.pam.AuthenticationStrategy)
如果配置了多个Realm ,则AuthenticationStrategy会协调Realm 以确定身份验证尝试成功或失败的条件(例如,如果一个Realm 成功而其他Realm 失败,则尝试成功吗?必须所有Realm 都成功吗? 第一?)。
-
Authorizer (org.apache.shiro.authz.Authorizer)
Authorizer 授权者是负责确定应用程序中用户访问控制的组件。 它是最终表明是否允许用户做某事的机制。 与身份验证器一样,授权者也知道如何与多个后端数据源进行协调以访问角色和权限信息。 授权者使用此信息来确定是否允许用户执行给定的操作。
-
SessionManager (org.apache.shiro.session.mgt.SessionManager)
SessionManager知道如何创建和管理用户会话生命周期,以便为所有环境中的用户提供可靠的会话体验。 这是安全框架领域中的一项独特功能-Shiro能够在任何环境中本地管理用户会话,即使没有Web / Servlet或EJB容器也可以。 默认情况下,Shiro将使用现有的会话机制(例如Servlet容器)(如果可用),但是如果没有这种机制(例如在独立应用程序或非Web环境中),它将使用其内置的企业会话管理来 提供相同的编程经验。 SessionDAO的存在是为了允许使用任何数据源来保留会话。
-
SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO)
SessionDAO代表SessionManager执行会话持久性(CRUD)操作。 这允许将任何数据存储插入会话管理基础结构。
-
CacheManager (org.apache.shiro.cache.CacheManager)
CacheManager创建和管理其他Shiro组件使用的Cache实例生命周期。 因为Shiro可以访问许多后端数据源以进行身份验证,授权和会话管理,所以缓存一直是框架中的一流架构功能,可以在使用这些数据源时提高性能。 可以将任何现代的开源和/或企业缓存产品插入Shiro,以提供快速有效的用户体验。
-
Cryptography (org.apache.shiro.crypto.*)
密码术是企业安全框架的自然补充。 Shiro的加密软件包包含易于使用和理解的加密密码,哈希(又名摘要)和不同编解码器实现的表示形式。 该软件包中的所有类都经过精心设计,以使其易于使用和理解。 使用Java的本机加密技术支持的任何人都知道,驯服它可能是具有挑战性的动物。 Shiro的加密API简化了复杂的Java机制,并使加密技术易于为普通凡人使用。
-
Realms (org.apache.shiro.realm.Realm)
领域充当Shiro与应用程序的安全数据之间的“桥梁”或“连接器”。 当真正需要与安全性相关的数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个Realms中查找许多此类内容。 您可以根据需要配置任意多个领域(通常每个数据源一个),并且Shiro将根据需要与它们进行协调,以进行身份验证和授权。
shiro的官网地址:https://shiro.apache.org
二.使用流程
1).web.xml添加shiro的过滤器DelegatingFilterProxy,让请求都进入shiro(过滤器要传入参数是spring容器中管理的ShiroFilterFactoryBean的id值,如果过滤器名和ShiroFilterFactoryBean的id值一致则不需要配置,不一致需要用过滤器配置参数和bean的id一致)
2).在applicationContext.xml中配置bean:ShiroFilterFactoryBean (这个bean和web.xml的过滤器有关).
a.但是ShiroFilterFactoryBean中要配置很多参数:登录地址,成功地址,未授权地址,过滤器链,缓存等,其中有个securityManager安全管理器;
b.securityManager的配置,也需要传入一个参数(shiroReaml),这个类是我们自定义写的登录和授权类,需要继承AuthorizingRealm抽象类;
c.我们自己写的shiroReaml类要加入spring容器时,可能也需要传入一些参数(如,密码的加密类);
d.配置加密的类(把加密的类HashedCredentialsMatcher,加入到容器中)。
------上面的介绍,在下面配置文件中体现-----
web.xml配置
<!-- Shiro filter start --> <!-- 配置shiro filter过滤器,被定义在classpath:applicationContext.xml文件里 DelegatingFilterProxy实际上是Filter的代理对象,默认情况下,spring会到IOC容器中查找和<filter-name>对应的filter bean, 也可以通过targetBeanName的初始化参数来配置filter bean的id。 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> <!-- 下面这个参数表示,applicationContext.xml(Shiro过滤器链)的名字,如果省略,配置器链名默认就是过滤器名字--> <init-param> <param-name>targetBeanName</param-name> <param-value>shiroFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Shiro filter end -->
applicationContext.xml配置
<!-- 10. Shiro认证权限配置--> <!-- ================ Shiro start ================ --> <!-- (1).声明凭证匹配器(密码加密用)--> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <!--注入算法--> <property name="hashAlgorithmName" value="md5"></property> <!--注入散列次数--> <property name="hashIterations" value="1"></property> </bean> <!-- (2).配置Realm,自己定义的shiroRealm,必须实现org.apache.shiro.realm.Realm这个接口--> <bean id="shiroReaml" class="com.cc8w.shiro.ShiroRealm"> <!--注入凭证匹配器--> <property name="credentialsMatcher" ref="credentialsMatcher"></property> </bean> <!-- (3). 配置cacheManager(缓存管理) --> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"> </bean> <!-- (4). 创建安全管理器securityManager,也就是shiro的核心--> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--注入reaml--> <property name="realm" ref="shiroReaml"></property> <!--缓存管理器--> <property name="cacheManager" ref="cacheManager" /> </bean> <!--(5).配置lifecycleBeanPostProcessor, 可以自动的来调用配置在spring IOC 容器中shiro bean的生命周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <!--(6).启用IOC容器中使用shiro的注解,但必须在配置 lifecycleBeanPostProcessor才可以使用--> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor" /> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <!-- (7). 配置过滤器链 shiroFilter , id必须和web.xml 文件中配置的DelegatingFilterProxy的filter-name一致--> <!-- 或shiroFilter的 id必须和web.xml里面的shiroFilter的 targetBeanName的值一样 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!--Shiro的核心安全接口,这个属性是必须的--> <property name="securityManager" ref="securityManager"></property> <!-- 要求登录时的链接(登录页面地址),非必须属性,属性会自动寻找web工程根目录下的"/login.jsp"页面--> <property name="loginUrl" value="/sys/login"></property> <!-- Shiro 的Web过滤器 id必须和web.xml里面的shiroFilter的 targetBeanName的值一样 --> <property name="successUrl" value="/index/welcome"></property> <!-- 用户访问未对其授权的资源时,所显示的连接 --> <property name="unauthorizedUrl" value="/sys/unauthorized"></property> <!-- 过滤器链的定义,从上往下顺序执行,一般将/**放在最后--> <!-- 配置哪些页面需要受保护 以及访问这些页面需要的权限 anon可以被匿名访问,或者说游客可以访问 authc 必须认证之后才能访问,即登录后才能访问的页面 --> <property name="filterChainDefinitions"> <value> <!-- /**=authc 所有url都必须认证通过才可以访问 --> <!-- 必须通过身份认证方可访问,身份认证的url必须和过虑器中指定的loginUrl一致 --> /login = authc /index.jsp*=anon <!-- /sys/doLogin*=anon 不用了--> <!-- /sys/login*=anon 不用了--> /sys/verifycode*=anon <!-- 如果访问/login/logout就是用Shiro注销session--> /sys/logout=logout <!--静态资源可以直接访问--> /static/**=anon <!-- 无权访问页面 --> /noauth = anon /api/**=anon /home/**=anon /crossdomain.xml=anon <!-- /admin.jsp = authc,roles[admin] --> <!-- /user.jsp = authc,roles[user] --> <!-- /** = anon所有url都可以匿名访问 --> <!-- /** = authc --> <!-- /*/* = authc --> <!-- /** = authc所有url都不可以匿名访问 必须放到最后面 --> /** = authc </value> </property> </bean> <!--(8). 配置未授权异常处理 --> <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <!-- 标签主体部分配置未授权显示页面,注意事项:页面路径不需要加.jsp后缀 --> <prop key="org.apache.shiro.authz.UnauthorizedException">/unauthorized</prop> </props> </property> </bean> <!--(9). 注解的支持 --> <context:component-scan base-package="com.cc8w.shiro"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan> <!-- ================ Shiro end ================ -->
总结要配置的组件有:
0)、配置applicationContext.xml
1)、配置securityManager,也就是shiro的核心。
2)、配置cacheManager(缓存管理)
3)、配置Realm,自己定义的shiroRealm,必须实现org.apache.shiro.realm.Realm这个接口
4)、配置lifecycleBeanPostProcessor,可以自动的来调用配置在spring IOC 容器中shiro bean的生命周期方法
5)、配置可以使用shiro注解,但必须在配置 lifecycleBeanPostProcessor才可以使用
6)、配置shiroFilter
自定义的验证类(登录判断和权限查询):
package com.cc8w.shiro; import com.cc8w.entity.UserActiveEntity; import com.cc8w.entity.UserEntity; import com.cc8w.service.PermssionService; import com.cc8w.service.RoleService; import com.cc8w.service.UserService; 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.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** * Shiro默认使用自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。 * 最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,AuthorizingRealm负责授权,通常自定义的realm继承AuthorizingRealm */ public class ShiroRealm extends AuthorizingRealm { //以下三个服务是普通service查询,从数据库查询用户及其角色权限信息 @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private PermssionService permssionService; @Override public String getName() { return this.getClass().getSimpleName(); } /* * 登录信息和用户验证信息验证(non-Javadoc) * @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("doGetAuthenticationInfo"); //注意:请求中账号与密码必须固定为username 跟password, 如果需要改动必须额外指定,authc.usernameParam=xxx authc.passwordParam=xxxx //1.从authenticationToken中获取身份信息,其实就是用户的登录名 String username = authenticationToken.getPrincipal().toString(); System.out.println(username); String password = authenticationToken.getCredentials().toString(); System.out.println(password); //2.根据用户名查询用户是否存在 UserEntity user=userService.queryUserByUserName(username); System.out.println(user); // 2.1没找到帐号 if (null == user) { throw new UnknownAccountException(); } // // 2.2校验用户状态 // if ("1".equals(user.getStatus())) { // throw new DisabledAccountException(); // } // if(user.getDisabled()){ // throw new LockedAccountException(); // } //2.3根据用户名去查询用户拥有哪些角色 List<String> roles= roleService.queryRolesByUserName(user.getUserName()); System.out.println(roles); //2.4根据用户名查询用户拥有哪些权限 List<String> permissions=permssionService.queryPermissionsByUserName(user.getUserName()); UserActiveEntity activeUser=new UserActiveEntity(user, roles, permissions); //3.返回认证信息(下面授权查询要用到) /** * 参数1 用户身份 * 参数2 用户在数据库里面存放的密码(加密的) * 参数3 当前类名 * 之后会把authenticationToken(或UsernamePasswordToken)的密码通过配置的加密规则,加密后和参数二做对比 */ SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(activeUser, user.getPassword(), this.getName()); //SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, realmName) System.out.println(info); return info; //返回后 有执行login(), assertCredentialsMatch密码对比等. } /* * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用,负责在应用程序中决定用户的访问控制的方法(non-Javadoc) * @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("doGetAuthorizationInfo"); //1.获得用户身份信息(PrincipalCollection有认证回调传来的第一个参数[activeUser]) UserActiveEntity activeUser = (UserActiveEntity) principalCollection.getPrimaryPrincipal(); //2.根据身份信息获取权限数据 SimpleAuthorizationInfo info=new SimpleAuthorizationInfo(); //3.根据用户查询用户的角色 (其实认证方法一并查询出来了,保存在UserActivePojo) List<String> roles = activeUser.getRoles(); if(null!=roles&&roles.size()>0) { info.addRoles(roles);//添加角色 } //4.根据用户查询用户的权限 List<String> permissions=activeUser.getPermissions(); if(null!=permissions&&permissions.size()>0) { info.addStringPermissions(permissions);//添加权限 } /** * 总结:本来授权->要查角色和权限都在本方法写(但是前端每请求查询一次权限,就会回调本方法一次, * 所以直接查数据库,对数据库有压力),所以最后, * 1.把查角色和权限方法写在了认证方法,然后封装成activeUser传递过来,这样Controller每次查权限,就不用查数据库了,直接在activeUser获取即可. * 2.缓存应该也可以解决 */ return info; } }
===========附带的一些类==========
一 . 要实现登录和授权,必不可少牵扯数据库,所以的有用户表,角色表,权限表,用户角色表(可放用户表中),角色权限表
1.用户表
2.角色表(t_shiro_role)
3.权限表(t_shiro_permission)
4.用户角色表(t_shiro_user_role)
5.角色权限表(t_shiro_role_permission)
二. 对上面数据表进行查询
1.对角色表的查询
RoleMapper.java
package com.cc8w.dao; import org.springframework.stereotype.Repository; import java.util.List; @Repository("roleMapper") public interface RoleMapper { public List queryRolesByUserName(String userName); }
RoleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cc8w.dao.RoleMapper"> <select id="queryRolesByUserName" resultType="java.lang.String"> select S.role_id,R.role_name from t_shiro_user_role as S left join t_shiro_role as R on S.role_id = R.role_id left join t_admins as A on A.id = S.user_id where A.username = #{userName} </select> </mapper>
2.对权限表操作
PermssionMapper.java
package com.cc8w.dao; import org.springframework.stereotype.Repository; import java.util.List; @Repository("permssionMapper") public interface PermssionMapper { public List queryPermissionsByUserName(String userName); }
PermssionMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cc8w.dao.PermssionMapper"> <select id="queryPermissionsByUserName" resultType="java.lang.String"> select RP.per_id,P.per_name,P.per_code from t_shiro_role_permission as RP left join t_shiro_permission as P on RP.per_id = P.per_id left join t_shiro_role as SR on SR.role_id=RP.role_id where RP.role_id in (select id from t_admins where username = #{userName}) </select> </mapper>
3.用户表的操作
package com.cc8w.dao; import com.cc8w.entity.UserEntity; public interface UserMapper { UserEntity queryUserByUserName(String userName); }
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cc8w.dao.UserMapper"> <!-- sql片段 --> <!-- 查询条件 --> <sql id="query_Name_where"> <if test="username!=null and username!=''"> and username = #{username} </if> </sql> <!-- 查询信息 --> <select id="queryUserByUserName" parameterType="string" resultType="com.cc8w.entity.UserEntity"> select * from t_admins t <where> <include refid="query_Name_where"/> </where> </select> <!-- 查询信息 --> <select id="queryUserById" parameterType="int" resultType="com.cc8w.entity.UserEntity"> select * from t_admins t where id=#{id} </select> </mapper>
4.用户、角色、权限的service
a.UserServiceImpl.java
package com.cc8w.service.impl; import com.cc8w.dao.UserMapper; import com.cc8w.entity.UserEntity; import com.cc8w.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service("userService") @Transactional public class UserServiceImpl implements UserService { @Autowired @Qualifier("userMapper") private UserMapper userMapper; public UserEntity queryUserByUserName(String userName){ return userMapper.queryUserByUserName(userName); } }
b.RoleServiceImpl.java
package com.cc8w.service.impl; import com.cc8w.dao.RoleMapper; import com.cc8w.service.RoleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service("roleService") @Transactional public class RoleServiceImpl implements RoleService { @Autowired @Qualifier("roleMapper") private RoleMapper roleMapper; @Override public List queryRolesByUserName(String userName) { return roleMapper.queryRolesByUserName(userName); } }
c.PermssionServiceImpl.java
package com.cc8w.service.impl; import com.cc8w.dao.PermssionMapper; import com.cc8w.service.PermssionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service("permssionService") @Transactional public class PermssionServiceImpl implements PermssionService { @Autowired @Qualifier("permssionMapper") private PermssionMapper permssionMapper; @Override public List queryPermissionsByUserName(String userName) { return permssionMapper.queryPermissionsByUserName(userName); } }
5.实体类
a.用户实体
package com.cc8w.entity; import lombok.Data; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotEmpty; import java.io.Serializable; @Data public class UserEntity implements Serializable { private int id; @NotEmpty(message = "姓名不能为空") private String userName; private String password; private String name; @Range(min = 1,max = 120) private int age; public UserEntity() { } public UserEntity(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } ...... }
b.用户角色权限实体
package com.cc8w.entity; import lombok.Data; import java.io.Serializable; import java.util.List; @Data public class UserActiveEntity implements Serializable { private UserEntity userEntity=null; private List<String> roles; private List<String> permissions;
public UserActiveEntity(){ } public UserActiveEntity(UserEntity userEntity, List<String> roles, List<String> permissions) { this.userEntity = userEntity; this.roles = roles; this.permissions = permissions; } ...... }
6.登录控制器进行验证
package com.cc8w.controller; import com.cc8w.util.VerifyCodeUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @RequestMapping("/sys") public class LoginController extends AdminBaseController { /** * 登录页面(applicationContext.xml配置路径) */ @RequestMapping("login") public String login(){ /** * authc登录拦截器工作原理 (重点:xml过滤链不用排除login) * authc拦截器有2个作用: * 1>登录认证 * 请求进来时,拦截并判断当前用户是否登录了,如果已经登录了放行, 如果没有登录,跳转到authc.loginUrl属性配置的路径,注意:默认是/login.jsp * * 2>执行登录认证 * 请求进来时,如果请求的路径为authc.loginUrl属性配置的路径(没配置,默认是/login.jsp)时,如果当前用户没有登录,authc这个拦截器会尝试获取请求中的账号跟密码值,然后比对ini配置文件或者realm中的用户列表,如果比对正确,直接执行登录操作,反之,抛异常,跳转到authc.loginUrl指定的路径。 * 注意:请求中账号与密码必须固定为username 跟password, 如果需要改动必须额外指定,authc.usernameParam=xxx authc.passwordParam=xxxx */ //如果登录失败从request中获取认证信息,shiroLoginFailure就是shiro异常类的全限定名 String exceptionClassName = (String) this.request.getAttribute("shiroLoginFailure"); //根据shiro返回的异常类路径判断,抛出指定异常信息 if(exceptionClassName!=null){ //最终会抛出异常处理器 if(UnknownAccountException.class.getName().equals(exceptionClassName)){ this.request.setAttribute("errorMsg","账号不存在"); }else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)){ this.request.setAttribute("errorMsg","用户名/密码错误"); }else{ this.request.setAttribute("errorMsg","其他异常信息"); } } //1.此方法不处理登录成功(认证成功),shiro认证成功会自动跳转到上一个请求路径 //2.登录失败还到login页面 //return "forward:/sys/login"; return "sys/login"; } /** * 没有权限 */ @RequestMapping("unauthorized") public String unauthorized(){ return "sys/unauthorized"; } /** * 登录提交 */ @RequestMapping("doLogin") public String doLogin(@RequestParam(name = "username") String username, @RequestParam(name = "password") String passowrd){ //可以不用了-提交数据全部交由shiro接管 //其实登录也可以用此action,同样可以完成验证 System.out.println(username); try { // 获取当前线程本地的Subject,会携带本次请求的相关信息(比如鉴权)如果存在的话 Subject subject = SecurityUtils.getSubject(); // UsernamePasswordToken可以满足大部分应用场景 // 可以自定义AuthenticationToken,但一定要和用于鉴权的Realm适配 UsernamePasswordToken token = new UsernamePasswordToken(username, passowrd); // 执行登录逻辑 subject.login(token); } catch (AuthenticationException e) { // 鉴权失败会抛出鉴权异常 return "Fail"; } // 鉴权成功则继续后续处理 // 一般返回sessionId或token // 本文仅作参考,直接返回字符串 return "success"; } /** * 登出操作 */ @RequestMapping("logout") public void logout(){ //可以不用了-交由shiro接管 } /** * 验证码 */ @RequestMapping("verifycode") public void verifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException { //response.setHeader("text/html; charset=UTF-8"); //禁止图片缓存 response.setHeader("Pragma","No-cache"); response.setHeader("Cache-Control","no-cache"); response.setDateHeader("Expires",0); response.setContentType("image/jpeg"); //生成随机字串 String verifyCode = VerifyCodeUtils.generateVerifyCode(4); //存入会话session request.getSession().setAttribute("verify_code", verifyCode.toLowerCase()); Object vakue = request.getSession().getAttribute("verify_code"); System.out.println(vakue); //生成图片 int width = 100;//宽 int height = 40;//高 VerifyCodeUtils.outputImage(width, height, response.getOutputStream(), verifyCode); //参数一:Image类对象 ,参数二:图片格式(非正式), //参数三:输出了(也可以是请求响应的response.getOutputStream().write(bytes),这样就输出到页面了) //ImageIO.write(verifyCode, "png", response.getOutputStream()); response.getOutputStream().flush(); response.getOutputStream().close(); return; } }
==============另有,自定义验证类UserRealm 和登录Controller,写法如下====
自定义xxxRealm
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.zyxx.sbm.entity.UserInfo; import com.zyxx.sbm.service.RolePermissionService; import com.zyxx.sbm.service.UserInfoService; import com.zyxx.sbm.service.UserRoleService; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; /** * 登录授权 */ public class LoginRelam extends AuthorizingRealm { @Autowired private UserInfoService userInfoService; @Autowired private UserRoleService userRoleService; @Autowired private RolePermissionService rolePermissionService; /** * 身份认证 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取基于用户名和密码的令牌:实际上这个authcToken是从LoginController里面currentUser.login(token)传过来的 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //根据用户名查找到用户信息 QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("account", token.getUsername()); UserInfo userInfo = userInfoService.getOne(queryWrapper); // 没找到帐号 if (null == userInfo) { throw new UnknownAccountException(); } // 校验用户状态 if ("1".equals(userInfo.getStatus())) { throw new DisabledAccountException(); } // 认证缓存信息 return new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getAccount()), getName()); } /** * 角色授权 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { UserInfo authorizingUser = (UserInfo) principalCollection.getPrimaryPrincipal(); if (null != authorizingUser) { //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //获得用户角色列表 Set<String> roleSigns = userRoleService.listUserRoleByUserId(authorizingUser.getId()); simpleAuthorizationInfo.addRoles(roleSigns); //获得权限列表 Set<String> permissionSigns = rolePermissionService.listRolePermissionByUserId(authorizingUser.getId()); simpleAuthorizationInfo.addStringPermissions(permissionSigns); return simpleAuthorizationInfo; } return null; } /** * 自定义加密规则 * * @param credentialsMatcher */ @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { // 自定义认证加密方式 CustomCredentialsMatcher customCredentialsMatcher = new CustomCredentialsMatcher(); // 设置自定义认证加密方式 super.setCredentialsMatcher(customCredentialsMatcher); } }
上面,我们已经配置了 shiro 的一系列操作,从登录验证、密码验证规则、用户授权等等,下面我们就开始登录,登录的操作,放在了 LoginController.java
import com.zyxx.common.consts.SystemConst; import com.zyxx.common.enums.StatusEnums; import com.zyxx.common.kaptcha.KaptchaUtil; import com.zyxx.common.shiro.SingletonLoginUtils; import com.zyxx.common.utils.PasswordUtils; import com.zyxx.common.utils.ResponseResult; import com.zyxx.sbm.entity.UserInfo; import com.zyxx.sbm.service.PermissionInfoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @ClassName LoginController * @Description * @Author Lizhou * @Date 2020-07-02 10:54:54 **/ @Api(tags = "后台管理端--登录") @Controller public class LoginController { @Autowired private PermissionInfoService permissionInfoService; @ApiOperation(value = "请求登录页面", notes = "请求登录页面") @GetMapping("login") public String init() { return "login"; } @ApiOperation(value = "请求主页面", notes = "请求主页面") @GetMapping("/") public String index() { return "index"; } @ApiOperation(value = "登录验证", notes = "登录验证") @ApiImplicitParams({ @ApiImplicitParam(name = "account", value = "账号", required = true), @ApiImplicitParam(name = "password", value = "密码", required = true), @ApiImplicitParam(name = "resCode", value = "验证码", required = true), @ApiImplicitParam(name = "rememberMe", value = "记住登录", required = true) }) @PostMapping("doLogin") @ResponseBody public ResponseResult doLogin(String account, String password, String resCode, Boolean rememberMe, HttpServletRequest request, HttpServletResponse response) throws Exception { // 验证码 if (!KaptchaUtil.validate(resCode, request)) { return ResponseResult.getInstance().error(StatusEnums.KAPTCH_ERROR); } // 验证帐号和密码 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(account, password); // 记住登录状态 token.setRememberMe(rememberMe); try { // 执行登录 subject.login(token); // 将用户保存到session中 UserInfo userInfo = (UserInfo) subject.getPrincipal(); request.getSession().setAttribute(SystemConst.SYSTEM_USER_SESSION, userInfo); return ResponseResult.getInstance().success(); } catch (UnknownAccountException e) { return ResponseResult.getInstance().error("账户不存在"); } catch (DisabledAccountException e) { return ResponseResult.getInstance().error("账户已被冻结"); } catch (IncorrectCredentialsException e) { return ResponseResult.getInstance().error("密码不正确"); } catch (ExcessiveAttemptsException e) { return ResponseResult.getInstance().error("密码连续输入错误超过5次,锁定半小时"); } catch (RuntimeException e) { return ResponseResult.getInstance().error("未知错误"); } } @ApiOperation(value = "登录成功,跳转主页面", notes = "登录成功,跳转主页面") @PostMapping("success") public String success() { return "redirect:/"; } @ApiOperation(value = "初始化菜单数据", notes = "初始化菜单数据") @GetMapping("initMenu") @ResponseBody public String initMenu() { return permissionInfoService.initMenu(); } @ApiOperation(value = "退出登录", notes = "退出登录") @GetMapping(value = "loginOut") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "login"; } }
当执行 subject.login(token); 时,就会进入登录验证中,对用户密码、状态进行检查,对用户授权等操作,登录的密码,一定是通过密码加密工具得到的,不然验证不通过
7.页面权限控制
使用的是 thymeleaf 模板引擎,我们需要在 html 文件中加入以下内容
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
1、判断当前用户有无此权限,通过权限标识
<button class="layui-btn" shiro:hasPermission="user_info_add"><i class="layui-icon"></i> 新增 </button>
2、与上面相反,判断当前用户无此权限,通过权限标识,没有时验证通过
<button class="layui-btn" shiro:lacksPermission="user_info_add"><i class="layui-icon"></i> 新增 </button>
3、判断当前用户有无以下全部权限,通过权限标识
<button class="layui-btn" shiro:hasAllPermissions="user_info_add"><i class="layui-icon"></i> 新增 </button>
4、判断当前用户有无以下任一权限,通过权限标识
<button class="layui-btn" shiro:hasAnyPermissions="user_info_add"><i class="layui-icon"></i> 新增 </button>
5、判断当前用户有无此角色,通过角色标识
<a shiro:hasRole="admin" href="admin.html">Administer the system</a>
6、与上面相反,判断当前用户无此角色,通过角色标识,没有时验证通过
<a shiro:lacksRole="admin" href="admin.html">Administer the system</a>
7、判断当前用户有无以下全部角色,通过角色标识
<a shiro:hasAllRoles="admin,role1,role2" href="admin.html">Administer the system</a>
8、判断当前用户有无以下任一角色,通过角色标识
<a shiro:hasAnyRoles="admin,role1,role2" href="admin.html">Administer the system</a>
=====关于密码加密,【用户注册时】和【认证时密码】加密规则必须一致===
(下面是MD5加密一次,没有加盐)
//登录测试(自定义Realm) @Test public void testRealmLogin(){ //1.创建一个安全管理器的工厂 Factory<SecurityManager> factory = new IniSecurityManagerFactory(); //2.在工厂中获取安全管理器 DefaultSecurityManager securityManager = (DefaultSecurityManager) factory.getInstance(); //---添加认证凭证配置start --如果Spring环境下(这些都是在xml配置的) //2.1 创建自定义Realm注入到安全管理器 ShiroRealm shiroRealm = new ShiroRealm();//(SpringM在bean控制,并可以配置散列加密相关) //2.1.1设置密码学相关加密方式 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //2.1.2设置加密方式 credentialsMatcher.setHashAlgorithmName("md5"); //2.1.3设置散列次数 credentialsMatcher.setHashIterations(1); //2.1.4将密码学凭证注入到自定义的Realm类中 shiroRealm.setCredentialsMatcher(credentialsMatcher); //---添加认证凭证配置end --如果Spring环境下(这些都是在xml配置的) securityManager.setRealm(shiroRealm); //3.将securityManager绑定到运行环境 SecurityUtils.setSecurityManager(securityManager); //4.获取Subject对象(将要登录的用户) Subject subject = SecurityUtils.getSubject(); //5.获取要登录用户的token,客户端传递过来的用户名和密码 String username = "zhangsan",password="123456"; UsernamePasswordToken token = new UsernamePasswordToken(username,password); try{ //6.登陆(认证) subject.login(token); logger.info("登录了"); }catch (IncorrectCredentialsException e ){ logger.info("密码不正确"); logger.info(e); }catch (UnknownAccountException e) { System.out.println("没有这个帐号"); }catch (AuthenticationException e) { e.printStackTrace(); } //如果登录成功了,可以获取subject中各种状态了 Boolean isAuth = subject.isAuthenticated(); System.out.println("认证状态:" + isAuth); // 7.授权 分为:基于角色授权 基于资源的授权 //7.1 基于角色授权 boolean permited = subject.hasRole("role1"); System.out.println("这是授权单个:"+permited); boolean hasAllRoles = subject.hasAllRoles(Arrays.asList("role1","role2","role3")); System.out.println("这个授权多个"+hasAllRoles); // 使用check方法进行授权,如果授权不通过会抛出异常 // subject.checkRole("role13"); try { subject.checkRole("roles1"); }catch (UnauthenticatedException e){ logger.info("没有这个角色"); //e.printStackTrace(); }catch (UnauthorizedException e){ logger.info("没有这个权限"); //e.printStackTrace(); } //7.2 基于资源的授权 //isPermitted传入权限标识符 boolean isPermitted = subject.isPermitted("user:query"); System.out.println("单个权限判断:"+isPermitted); boolean isPermittedAll = subject.isPermittedAll("user:query","user:adb","user:add"); System.out.println("多个权限判断"+isPermittedAll); // 使用check方法进行授权,如果授权不通过会抛出异常 try { subject.checkPermission("user:adb"); }catch (UnauthenticatedException e){ logger.info("没有这个角色"); //e.printStackTrace(); }catch (UnauthorizedException e){ logger.info("没有这个权限"); //e.printStackTrace(); } }
注册时,可对用户密码进行加密
public String md5(String password,String salt=""){ //加密方式 String algorithmName = "MD5"; //盐值 ByteSource byteSalt = ByteSource.Util.bytes(salt); //加密次数 int hashIterations = 1; SimpleHash result = new SimpleHash(algorithmName, password, byteSalt, hashIterations); //Md2Hash Md5Hash Sha1Hash Sha256Hash Sha384Hash Sha512Hash 最后都是调用SimpleHash加密 //Md5Hash r = new Md5Hash(password,byteSalt,hashIterations); return result.toHex(); }
之前写的:https://www.cnblogs.com/fps2tao/p/13570008.html
转: https://blog.csdn.net/weixin_67695384/article/details/125420185
http://t.zoukankan.com/Hxinguan-p-6188897.html
https://www.jianshu.com/p/af2ca32ecf12
https://zhuanlan.zhihu.com/p/89536348
https://www.codenong.com/cs106115507/
https://blog.csdn.net/baidu_36697353/article/details/70837775
https://blog.csdn.net/wenxingchen/article/details/103069227
shiro登录:
https://www.cnblogs.com/rchao/p/10983355.html
https://blog.csdn.net/qq_40065776/article/details/117159986
https://blog.csdn.net/xing_hung/article/details/122753555
https://www.jianshu.com/p/b3c953c8a988
https://blog.csdn.net/weixin_43818197/article/details/104410475