Apache Shiro
What is Apache Shiro?
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦,但是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。
Shiro能做什么呢?
- 验证用户身份
- 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
- 在非 web 或 EJB 容器的环境下可以任意使用Session API
- 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
- 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
- 支持单点登录(SSO)功能
- 支持提供“Remember Me”服务,获取用户关联信息而无需登录
…
等等——都集成到一个有凝聚力的易于使用的API。
Shiro 致力在所有应用环境下实现上述功能,小到命令行应用程序,大到企业应用中,而且不需要借助第三方框架、容器、应用服务器等。当然 Shiro 的目的是尽量的融入到这样的应用环境中去,但也可以在它们之外的任何环境下开箱即用。
Apache Shiro Features 特性
Apache Shiro是一个全面的、蕴含丰富功能的安全框架。下图为描述Shiro功能的框架图:
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
- Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
- Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:
- Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
- 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
- 并发:Apache Shiro 支持多线程应用程序的并发特性。
- 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
- “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
- “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro
High-Level Overview 高级概述
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
- Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
- Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
Shiro认证过程
Shiro授权的过程
新建一个工程
首先引入pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <!-- <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--shiro需要的jar包--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <!--mybatis的配置--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.8.0-alpha2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- aspectj的支持 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.12</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency> <!-- shiro ehcache --> <!-- 包含支持UI模版(Velocity,FreeMarker,JasperReports), 邮件服务, 脚本服务(JRuby), 缓存Cache(EHCache), 任务计划Scheduling(uartz)。 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!--热部署.jar ,当你修改代码后,服务器会自动重启 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!-- thmleaf模板依赖. --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency>
User.java
private Integer id; private String username; private String password; private List<Roles> roles; //省略getter setter
Role.java
public class Roles { private Integer id; private String roleName; private String description;
//省略getter setter
}
Permission.java
public class Permission { private Integer id; private String permissionName; private String permissionUrl; private String description; // Getter Setter省略 }
Shiroconfige.java
package com.cmbc.exmaple.Shiro.realm; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.filter.authc.LogoutFilter; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; /** * @Author zly * @Date 2018/9/6 17:14 */ @Configuration public class ShiroConfiguration { @Bean public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 拦截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 设置login URL shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/LoginSuccess.action"); // 未授权的页面 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized.action"); // 设置登录的URL为匿名访问,因为一开始没有用户验证 filterChainDefinitionMap.put("/login.action", "anon"); filterChainDefinitionMap.put("/Exception.class", "anon"); filterChainDefinitionMap.put("/*.action", "authc"); // 退出系统的过滤器 filterChainDefinitionMap.put("/logout", "logout"); // 现在资源的角色 filterChainDefinitionMap.put("/admin.html", "roles[admin]"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public Myrealm myShiroRealm() { Myrealm myShiroRealm = new Myrealm(); return myShiroRealm; } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注入自定义的realm; securityManager.setRealm(myShiroRealm()); return securityManager; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
自定义Myrealm
package com.cmbc.exmaple.Shiro.realm; import com.cmbc.exmaple.Shiro.entity.Permission; import com.cmbc.exmaple.Shiro.entity.Roles; import com.cmbc.exmaple.Shiro.entity.User; import com.cmbc.exmaple.Shiro.service.PermissionService; import com.cmbc.exmaple.Shiro.service.UserService; import com.cmbc.exmaple.Shiro.service.UserServices; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; 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.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import java.util.HashSet; import java.util.Set; /** * @Author zly * @Date 2018/9/6 16:19 */ public class Myrealm extends AuthorizingRealm { @Autowired private UserServices userServices; @Autowired PermissionService permissionService; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // System.out.println("doGetAuthenticationInfo:" + token); System.out.println("进入了登录验证"); // 将AuthenticationToken强转为AuthenticationToken对象 UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 获得从表单传过来的用户名 String username = upToken.getUsername(); System.out.println("表单传过来的"+username); /*// 从数据库查看是否存在用户 UserService userService = new UserService(); // 如果用户不存在,抛此异常 if (!userService.selectUsername(username)) { throw new UnknownAccountException("无此用户名!"); } Object principal = username; // 从数据库中查询的密码 Object credentials = userService.selectPassword(username); // 当前realm对象的名称,调用分类的getName() String realmName = this.getName();*/ User user = userServices.getByUserName(username); System.out.println(user); if (user==null){ throw new UnknownAccountException("无此用户名!"); }else { Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("user", user); // 用户密码的比对是Shiro帮我们完成的 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, user.getPassword(), getName()); return info; } /* SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, user.getPassword(), getName()); return info;*/ } // 用于授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("进入了授权"); //System.out.println("MyShiroRealm的doGetAuthorizationInfo授权方法执行"); String username = (String)principals.getPrimaryPrincipal(); User user = userServices.getByUserName(username); SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(user.getId()),SecurityUtils.getSubject().getPrincipals()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for (Roles roles : user.getRoles()) { info.addRole(roles.getRoleName()); } for (Permission permission : permissionService.getByUserId(user.getId())) { info.addStringPermission(permission.getPermissionName()); } //System.out.println("ShiroRealm AuthorizationInfo:" + principal.toString()); /*// 模拟数据库 根据用户名来查询数据库赋予用户角色,权限(查数据库) Set<String> roles = new HashSet<>(); Set<String> permissions = new HashSet<>(); roles.add("user"); permissions.add("user:query"); if ("admin".equals(principal)) { roles.add("admin"); permissions.add("admin:query"); }*/ /* if ("user".equals(principal)) { roles.add("admin"); permissions.add("admin:query"); }*/ //天加权限 return info; // return null; } }
yml 配置文件
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://99.48.232.39:3306/boot?characterEncoding=utf8 password: 1qaz@WSX username: root # jpa: # hibernate: # ddl-auto: update # show-sql: true # database: mysql thymeleaf: servlet: content-type: text/html mode: LEGACYHTML5 cache: false check-template-location: true prefix: classpath:/templates/ suffix: .html mybatis: type-aliases-package: com.cmbc.exmaple.Shiro.entity mapper-locations: classpath:com/cmbc/exmaple/Shiro/dao/mapper/*.xml server: port: 8088 #mybatis: # type-aliases-package: com.cmbc.exmaple.entity # mapper-locations: classpath:com/cmbc/dao/mapper/*.xml
dao层
UserDao
package com.cmbc.exmaple.Shiro.dao; import com.cmbc.exmaple.Shiro.entity.User; import io.swagger.models.auth.In; import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; /** * @Author zly * @Date 2018/11/12 10:05 */ public interface UserDao { List<User> getByMap(Map<String,Object> map); User getById(@Param("id") Integer id); Integer create(User user); int update(User user); int delete(@Param("id") Integer id); User getByUserName(@Param("username") String username); }
RoleDao
package com.cmbc.exmaple.Shiro.dao; import com.cmbc.exmaple.Shiro.entity.Roles; import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; /** * @Author zly * @Date 2018/11/12 11:37 */ public interface RoleDao { List<Roles> getByMap(Map<String, Object> map); Roles getById(@Param("id") Integer id); Integer create(Roles role); int update(Roles role); int delete(@Param("id") Integer id); }
PermissionDao
package com.cmbc.exmaple.Shiro.dao; import com.cmbc.exmaple.Shiro.entity.Permission; import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; /** * @Author zly * @Date 2018/11/12 11:47 */ public interface PermissionDao { List<Permission> getByMap(Map<String, Object> map); Permission getById(@Param("id") Integer id); Integer create(Permission permission); int update(Permission permission); int delete(@Param("id") Integer id); List<Permission> getList(); List<Permission> getByUserId(@Param("userId") Integer userId); }
service层Mapper 最后会有源码链接(数据库也有)
这里贴一下loginController 和UserController
LoginController
package com.cmbc.exmaple.Shiro.controller; import com.cmbc.exmaple.Shiro.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.ExpiredCredentialsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Map; /** * @Author zly * @Date 2018/9/7 14:10 */ @Controller public class LoginContorller { @RequestMapping("login.action") public String doLogin(User user, Map<String, Object> map){ System.out.println(user.getPassword()+""+user.getUsername()); Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()){ String msg=""; UsernamePasswordToken token =new UsernamePasswordToken(user.getUsername(),user.getPassword()); token.setRememberMe(true); try { subject.login(token); return "redirect:/LoginSuccess.action"; } catch (IncorrectCredentialsException e) { msg = "登录密码错误"; System.out.println("登录密码错误!!!" + e); } catch (ExcessiveAttemptsException e) { msg = "登录失败次数过多"; System.out.println("登录失败次数过多!!!" + e); } catch (LockedAccountException e) { msg = "帐号已被锁定"; System.out.println("帐号已被锁定!!!" + e); } catch (DisabledAccountException e) { msg = "帐号已被禁用"; System.out.println("帐号已被禁用!!!" + e); } catch (ExpiredCredentialsException e) { msg = "帐号已过期"; System.out.println("帐号已过期!!!" + e); } catch (UnknownAccountException e) { msg = "帐号不存在"; System.out.println("帐号不存在!!!" + e); } catch (UnauthorizedException e) { msg = "您没有得到相应的授权!"; System.out.println("您没有得到相应的授权!" + e); } map.put("msg", msg); return "/login"; } // 登录成功,重定向到LoginSuccess.action return "redirect:/LoginSuccess.action"; } }
UserController
package com.cmbc.exmaple.Shiro.controller; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @Author zly * @Date 2018/9/7 14:08 */ @Controller public class UserController { // 登录的url @RequestMapping({ "/login", "/" }) public String indexHtml() { return "login"; } // 属于user角色@RequiresRoles("user") // 必须同时属于user和admin角@RequiresRoles({ "user", "admin" }) // 属于user或者admin之一;修改logical为OR 即可@RequiresRoles(value = { "user", "admin"}, // logical = Logical.OR) @RequestMapping("/showUserHtml.action") @RequiresRoles(value = { "user", "admin"},logical = Logical.OR) //@RequiresPermissions("user:query") public String userHtml() { return "user"; } @RequestMapping("/showAdminHtml.action") @RequiresRoles("admin") @RequiresPermissions("admin:query") public String adminHtml() { return "admin"; } @RequestMapping("/unauthorized.action") public String unauthorized() { return "/abc"; } @RequestMapping("/LoginSuccess.action") public String listHtml() { return "list"; } @RequestMapping("/error.action") public String error() { int a=1/0; return "abc"; } }
配置好之后启动访问localhost8088/
登录之后
然后username 改成user 之后访问admin html 会出现:
有什么问题多多指教