Apache Shiro 是Java的 安全框架 ,提供了认证(Authentication)、授权(Authorization)、会话(Session)管理、加密(Cryptography)等功能,且Shiro与Spring Security等安全框架相比具有简单性、灵活性、支持细粒度鉴权、支持一级缓存等, 还有Shiro不跟任何容器(Tomcat等)和框架(Sping等)捆绑,可以独立运行,这也造就了Shiro不仅仅是可以用在Java EE上还可以用在Java SE上 。
在开始之前,首先了解一下Shiro的四大功能,俗话说“知己知彼百战不殆”。
认证就是用户访问系统的时候,系统要验证用户身份的合法性,比如我们通常所说的“登录”就是认证的一种方式,只有登录成功了之后我们才能访问相应的资源。在Shiro中,我们可以将用户理解为 Subject 主体,在用户身份认证的时候,用户需要提供能证明他身份的信息,如用户名、密码等,用户所提供的这些用户名、密码则对应Shiro中的Principal、 Credentials,即在Subject进行身份认证的时候,需要提供相应的Principal、 Credentials,对应的代码如下:
UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(token); //提交认证 复制代码
我们知道Http协议是 无状态 的,所以用户认证成功后怎么才能保持认证成功的状态呢?如果是我们开发的话一般都是登录成功后将Session储存在服务器,然后再将Session返回给用户,之后的请求用户都将这个Session带上,然后服务器根据用户请求携带的Session和服务器储存的Session进行比较来判断用户是否已认证。但是使用Shiro后, Shiro已经帮我们做好这个了(下面介绍的会话管理),是不是feel爽~
授权可以理解为访问控制,在用户认证(登录)成功之后,系统对用户访问资源的权限进行控制,即确定什么用户能访问什么资源,如普通用户不能访问后台,但是管理员可以。在这里我们还需要认识几个概念,资源(Resource)、角色(Role)、权限(Permission),上面提到的Subject主体可以有多个角色,每个角色又对应多个资源的多个权限,这种 基于资源的访问控制 可以实现细粒度的权限。对主体设置角色、权限的代码如下:
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 添加用户的角色 authorizationInfo.addRoles(roleIdList); // 添加用户的权限 authorizationInfo.addStringPermissions(resourceIdList); 复制代码
如果要实现这样的授权功能,我们必定需要设计一个用户组、权限,给每个方法或者URL加上判断,是否当前登录的用户满足条件。但是使用Shiro后, Shiro也帮我们帮这些都做好了 。
会话管理的会话即Session,所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。在Shiro中,与用户有关的一切信息都可以通过Shiro的接口获得,和用户的会话Session也都由Shiro管理。如实现“记住我”或者“下次自动登录”的功能,如果要自己去开发的话,估计又得话不少时间。但是使用Shiro后, Shiro也帮我们帮这些都做好了 。
用户密码明文保存是不是安全,应不应该MD5加密,是不是应该加盐,又要写密码加密的代码。 这些Shiro已经帮你做好了 。
从整体概念上理解,Shiro的体系架构有三个主要的概念,Subject(主体),Security Manager (安全管理器)和 Realms (域)。
主体是当前正在操作的用户的特定数据集合。主体可以是一个人,也可以代表第三方服务,守护进程,定时任务或类似的东西,也就是几乎所有与该应用进行交互的事物。所有Subject都绑定到 SecurityManager
,与Subject的所有交互都会委托给 SecurityManager,可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。
安全管理器,即所有与安全有关的操作都会与 SecurityManager
交互,且它 管理着所有Subject 可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成DispatcherServlet前端控制器, 一般来说,一个应用只会存在一个SecurityManager实例 。
域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm得到用户相应的角色 / 权限进行验证用户是否能进行操作,即Realms作为Shiro与应用程序安全数据之间的“桥梁”。从这个意义上讲,Realm实质上是一个安全相关的 DAO ,它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。其中Realm有2个方法, doGetAuthenticationInfo
用来认证, doGetAuthorizationInfo
用来授权。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>shiro</groupId> <artifactId>shiro</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>shiro Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!--Sping核心依赖--> <!-- https://mvnrepository.com/artifact/org.springframework/spring-core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-web --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.3.RELEASE</version> <scope>test</scope> </dependency> <!--Mybatis依赖--> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!--MySQL连接驱动--> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>shiro</finalName> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build> </project> 复制代码
为了减少篇幅,只做简单介绍,详情可以查看源码,数据库文件在本项目根目录。
id
, name
两个字段,分别对应资源id和权限。 id
, name
两个字段,分别对应角色id和角色名。 id
, roleid
, resid
三个字段,分别对应自增id、角色id和资源id。 id
, username
, password
三个字段,分别对应自增id、用户名和密码。 id
, uid
, rid
三个字段,分别对应自增id、用户id、和角色id。 public interface AccountDao { User findUserByUsername(String username); List<Role> findRoleByUserId(int id); List<Resource> findResourceByUserId(int id); } 复制代码
public interface AccountService { User findUserByUsername(String username); List<Role> findRoleByUserId(int id); List<Resource> findResourceByUserId(int id); boolean login(User user); } 复制代码
package com.shiro.service.impl; import com.shiro.dao.AccountDao; import com.shiro.entity.Role; import com.shiro.entity.User; import com.shiro.service.AccountService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; /** * @program: shiro * @description: * @author: Xue 8 * @create: 2019-02-01 15:37 **/ @Service public class AccountServiceImpl implements AccountService { @Resource AccountDao accountDao; /** * @description: 根据用户名查找用户信息 * @param: [username] * @return: com.shiro.entity.User * @author: Xue 8 * @date: 2019/2/1 */ @Override public User findUserByUsername(String username) { return accountDao.findUserByUsername(username); } @Override public List<Role> findRoleByUserId(int id) { return accountDao.findRoleByUserId(id); } @Override public List<com.shiro.entity.Resource> findResourceByUserId(int id) { return accountDao.findResourceByUserId(id); } public boolean login(User user){ // 获取当前用户对象subject Subject subject = SecurityUtils.getSubject(); System.out.println("subject:" + subject.toString()); // 创建用户名/密码身份证验证Token UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); System.out.println("token" + token); try { subject.login(token); System.out.println("登录成功"); return true; } catch (Exception e) { System.out.println("登录失败" + e); return false; } } } 复制代码
package com.shiro.service.impl; import com.shiro.entity.Role; import com.shiro.entity.User; import com.shiro.service.AccountService; 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 javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * @program: shiro * @description: * @author: Xue 8 * @create: 2019-02-01 15:16 **/ public class MyRealm extends AuthorizingRealm { @Resource AccountService accountService; /** * 身份认证的方法 认证成功获取身份验证信息 * 这里最主要的是user.login(token);这里有一个参数token,这个token就是用户输入的用户密码, * 我们平时可能会用一个对象user来封装用户名和密码,shiro用的是token,这个是控制层的代码,还没到shiro, * 当调用user.login(token)后,就交给shiro去处理了,接下shiro应该是去token中取出用户名,然后根据用户去查数据库, * 把数据库中的密码查出来。这部分代码一般都是要求我们自定义实现,自定义一个realm,重写doGetAuthenticationInfo方法 **/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取用户输入的用户名和密码 // 实际上这个token是从UserResource面currentUser.login(token)传过来的 // 两个token的引用都是一样的 String username = (String) authenticationToken.getPrincipal(); // 密码要用字符数组来接受 因为UsernamePasswordToken(username, password) 储存密码的时候是将字符串类型转成字符数组的 查看源码可以看出 String password = new String((char[]) authenticationToken.getCredentials()); // 调用service 根据用户名查询用户信息 User user = accountService.findUserByUsername(username); // String password = user.getPassword(); // 判断用户是否存在 不存在则抛出异常 if (user != null) { // 判断用户密码是否匹配 匹配则不匹配则抛出异常 if (user.getPassword().equals(password)) { // 登录成功 把用户信息储存在Session中 Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("userSession", user); session.setAttribute("userSessionId", user.getId()); // 认证成功 返回一个AuthenticationInfo的实现 return new SimpleAuthenticationInfo(username, password, getName()); } else { System.out.println("密码不正确"); throw new IncorrectCredentialsException(); } } else { System.out.println("账号不存在"); throw new UnknownAccountException(); } } /** * 授权的方法 * 1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候; * * 2、@RequiresRoles("admin") :在方法上加注解的时候; * * 3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。 * 4、xml配置权限的时候也会走 **/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("授权"); // 从principalCollection获取用户信息 // 如果doGetAuthenticationInfo(user,password,getName()); 传入的是user类型的数据 那这里getPrimaryPrincipal获取到的也是user类型的数据 String username = (String) principalCollection.getPrimaryPrincipal(); User user = accountService.findUserByUsername(username); // 获取该用户的所有角色 List<Role> roleList = accountService.findRoleByUserId(user.getId()); // 将角色的id放到一个String列表中 因为authorizationInfo.addRoles()方法只支持角色的String列表或者单个角色String List<String> roleIdList = new ArrayList<String>(); for (Role role:roleList) { roleIdList.add(role.getName()); } // 获取该用户的所有权限 List<com.shiro.entity.Resource> resourceList = accountService.findResourceByUserId(user.getId()); List<String> resourceIdList = new ArrayList<String>(); // 将权限id放到一个String列表中 因为authorizationInfo.addRoles()方法只支持角色的String列表或者单个角色String for (com.shiro.entity.Resource resource:resourceList) { resourceIdList.add(resource.getName()); } System.out.println("授权11"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 添加用户的角色 authorizationInfo.addRoles(roleIdList); // 添加用户的权限 authorizationInfo.addStringPermissions(resourceIdList); return authorizationInfo; } } 复制代码
package com.shiro.controller; import com.shiro.entity.User; import com.shiro.service.AccountService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; /** * @program: shiro * @description: * @author: Xue 8 * @create: 2019-02-01 13:14 **/ @Controller public class AccountController { @Resource AccountService accountService; @Resource HttpServletRequest servletRequest; @RequestMapping(value = "/home") public String home(){ return "home"; } @RequestMapping(value = "/login", method = RequestMethod.GET) public String getLogin(){ return "login"; } @RequestMapping(value = "/login", method = RequestMethod.POST) public String doLogin(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password){ User user = new User(); user.setUsername(username); user.setPassword(password); if (accountService.login(user)) { return "/home"; } return "/login"; } } 复制代码
以 GET
方法访问 /login
的时候,会出现登录页面,输入账号密码点击登录数据将以 POST
方式提交给 /login
,如果账号密码匹配返回 /home
的页面,否则返回 /login
的页面。 /home
页面只有在登录且有权限的情况下才可以访问, 未登录情况下 访问会转跳 /login
页面,这个在Shiro的配置文件里面配置。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--开启扫描注册--> <context:component-scan base-package="com.shiro"></context:component-scan> <!--读取properties配置--> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:jdbcConfig.properties"></property> </bean> <!--配置数据源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${driverClassName}"></property> <property name="username" value="${username}"></property> <property name="password" value="${password}"></property> <property name="url" value="${url}"></property> </bean> <!--配置session工厂--> <bean id="sessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="configLocation" value="classpath:mybatis-config.xml"></property> <property name="mapperLocations" value="classpath:mapping/*.xml"></property> </bean> <!--配置扫描mapping--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.shiro.dao"></property> <property name="sqlSessionFactoryBeanName" value="sessionFactoryBean"></property> </bean> </beans> 复制代码
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm"></property> </bean> <bean id="myRealm" class="com.shiro.service.impl.MyRealm"> <!--关闭权限缓存 不然doGetAuthorizationInfo授权方法不执行--> <property name="authorizationCachingEnabled" value="false"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"></property> <property name="successUrl" value="/success"></property> <!--登录页面--> <property name="loginUrl" value="/login"></property> <property name="filterChainDefinitions"> <value> <!--配置`/home`只有拥有`admin`角色的用户才可以访问--> /home = authc,roles[admin] </value> </property> </bean> </beans> 复制代码
打开 http://localhost:8080/login
登录页面,填写正确用户名和密码登录
登录成功 转跳成功页面
清除浏览器cookie之后(未登录状态),打开http://localhost:8080/home
页面,自动转跳到了
/login
登录页面(即没有权限访问),登录账户,再次打开
http://localhost:8080/home
页面即可正常访问。