一、基本名词解释
Apache Shiro是一个强大易用的Java安全框架。它可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存、单点登录等等,而且它的API也十分简洁易用,所以现在有很多人都在使用它。它的基本能功能点如图所示:
从图上我们可以看出Shiro的四大核心功能:
Authentication(身份验证):简称为“登录”,即证明用户是谁。
Authorization(授权):访问控制的过程,即决定是否有权限去访问受保护的资源。
Session Management(会话管理):管理用户特定的会话,即使在非 Web 或 EJB 应用程序。
Cryptography(加密):通过使用加密算法保持数据安全
我们同时也可以看看Shiro的架构长什么样子:
同样的虚线框框圈着的是Shiro3大核心组件:
Subject :正与系统进行交互的人,或某一个第三方服务。所有 Subject 实例都被绑定到(且这是必须的)一个SecurityManager 上。
SecurityManager:Shiro 架构的心脏,用来协调内部各安全组件,管理内部组件实例,并通过它来提供安全管理的各种服务。当 Shiro 与一个 Subject 进行交互时,实质上是幕后的 SecurityManager 处理所有繁重的 Subject 安全操作。
Realms :本质上是一个特定安全的 DAO。当配置 Shiro 时,必须指定至少一个 Realm 用来进行身份验证和/或授权。Shiro 提供了多种可用的 Realms 来获取安全相关的数据。如关系数据库(JDBC),INI 及属性文件等。可以定义自己 Realm 实现来代表自定义的数据源。
以上是些基本的名称解释。如需要查看更详细的请参考开涛大神的博客。
二、准备工作
整合程序沿用之前的例子Maven+spring+Spring MVC+MyBatis+MySQL整合SSM框架,现在我们需要在此基础上继续整合进Shiro。
三、开始整合
1.加入jar包
整合demo用的是maven对依赖进行管理。我们需要在pom.xml里加上配置:
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.1.32</version>
- </dependency>
- <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>
里面我加入了阿里的fastjson。我用fastjson替换了jackson(理由:没有什么理由,就是喜欢用了.....)
2.spring-mvc.xml的配置
这是替换掉jackson的配置
- <mvc:annotation-driven>
- <mvc:message-converters>
- <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
- <bean class="org.springframework.http.converter.FormHttpMessageConverter" />
- <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
- <!--<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />-->
- <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" />
- </mvc:message-converters>
- </mvc:annotation-driven>
- <mvc:default-servlet-handler />
- <!-- 避免IE执行AJAX时,返回JSON出现下载文件 -->
- <!-- 支持JSON数据格式 -->
- <bean
- class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
- <property name="messageConverters">
- <list>
- <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/><!-- 解析导出文件byte流 -->
- <ref bean="fastJsonHttpMessageConverter" />
- <!--
- <ref bean="mappingJacksonHttpMessageConverter" />
- -->
- </list>
- </property>
- </bean>
- <!--<bean id="mappingJacksonHttpMessageConverter"
- class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
- </bean>-->
- <!-- 使用fastJson来支持JSON数据格式 -->
- <bean id="fastJsonHttpMessageConverter" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
- <property name="supportedMediaTypes">
- <list>
- <value>text/html;charset=UTF-8</value>
- <value>application/json</value>
- </list>
- </property>
- <property name="features">
- <list>
- <value>WriteMapNullValue</value>
- <value>QuoteFieldNames</value>
- </list>
- </property>
- </bean>
- <!-- 配置 Shiro 的 Filter -->
- <filter>
- <description>shiro 权限拦截</description>
- <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>
- </filter>
- <filter-mapping>
- <filter-name>shiroFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
- <?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-3.1.xsd">
- <!-- 启用shrio授权注解拦截方式 -->
- <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
- <!-- 装配 securityManager -->
- <property name="securityManager" ref="securityManager"/>
- <!-- 配置登陆页面 -->
- <property name="loginUrl" value="/index.jsp"/>
- <!-- 登陆成功后的一面 -->
- <property name="successUrl" value="/jsp/success.jsp"/>
- <span style="white-space:pre"> </span><property name="unauthorizedUrl" value="/jsp/unauthorized.jsp"/>
- <!-- 具体配置需要拦截哪些 URL, 以及访问对应的 URL 时使用 Shiro 的什么 Filter 进行拦截. -->
- <property name="filterChainDefinitions">
- <value>
- /index.jsp=anon
- /jsp/success.jsp=anon
- /jsp/fail.jsp=anon
- /jsp/user.jsp = roles[user]
- /jsp/admin.jsp = roles[admin]
- /logout = logout
- </value>
- </property>
- </bean>
- <!-- 配置缓存管理器 -->
- <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
- <!-- 指定 ehcache 的配置文件 -->
- <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
- </bean>
- <!-- 配置进行授权和认证的 Realm -->
- <bean id="myRealm" class="com.gray.base.shiro.ShiroDbRealm">
- <property name="userService" ref="userService" />
- </bean>
- <bean id="userService" class="com.gray.user.service.impl.UserServiceImpl" />
- <!-- 配置 Shiro 的 SecurityManager Bean. -->
- <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
- <property name="cacheManager" ref="cacheManager"/>
- <property name="realm" ref="myRealm"/>
- <property name="sessionMode" value="native">
- </property>
- </bean>
- <!-- 配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法. -->
- <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
- </beans>
- <%@ page language="java" contentType="text/html; charset=utf-8"pageEncoding="utf-8"%>
- <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
- <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <title></title>
- </head>
- <body>
- ${successMsg } Welcome! <shiro:principal/>
- <br><br>
- <shiro:hasAnyRoles name="user">
- <a href="/jsp/user.jsp">User Page</a>
- </shiro:hasAnyRoles>
- <br><br>
- <shiro:hasAnyRoles name="admin">
- <a href="/jsp/admin.jsp">Admin Page</a>
- </shiro:hasAnyRoles>
- <br><br>
- <a href="../test/logout.do">Logout</a>
- </body>
- </html>
6.1 Controller层的编写
这次整合我么主要要实现登陆和登出功能,因此controller层我们这样写:
- package com.gray.user.controller;
- import java.io.IOException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- 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.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.Model;
- import org.springframework.web.bind.annotation.RequestMapping;
- import com.gray.user.entity.User;
- import com.gray.user.service.impl.UserServiceImpl;
- @Controller
- @RequestMapping("/test")
- public class LoginController {
- @Autowired
- private UserServiceImpl userService;
- @RequestMapping("/dologin.do") //url
- public String dologin(User user, Model model){
- String info = loginUser(user);
- if (!"SUCC".equals(info)) {
- model.addAttribute("failMsg", "用户不存在或密码错误!");
- return "/jsp/fail";
- }else{
- model.addAttribute("successMsg", "登陆成功!");//返回到页面说夹带的参数
- model.addAttribute("name", user.getUsername());
- return "/jsp/success";//返回的页面
- }
- }
- @RequestMapping("/logout.do")
- public void logout(HttpServletRequest request,HttpServletResponse response) throws IOException{
- Subject subject = SecurityUtils.getSubject();
- if (subject != null) {
- try{
- subject.logout();
- }catch(Exception ex){
- }
- }
- response.sendRedirect("/index.jsp");
- }
- private String loginUser(User user) {
- if (isRelogin(user)) return "SUCC"; // 如果已经登陆,无需重新登录
- return shiroLogin(user); // 调用shiro的登陆验证
- }
- private String shiroLogin(User user) {
- // 组装token,包括客户公司名称、简称、客户编号、用户名称;密码
- UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword().toCharArray(), null);
- token.setRememberMe(true);
- // shiro登陆验证
- try {
- SecurityUtils.getSubject().login(token);
- } catch (UnknownAccountException ex) {
- return "用户不存在或者密码错误!";
- } catch (IncorrectCredentialsException ex) {
- return "用户不存在或者密码错误!";
- } catch (AuthenticationException ex) {
- return ex.getMessage(); // 自定义报错信息
- } catch (Exception ex) {
- ex.printStackTrace();
- return "内部错误,请重试!";
- }
- return "SUCC";
- }
- private boolean isRelogin(User user) {
- Subject us = SecurityUtils.getSubject();
- if (us.isAuthenticated()) {
- return true; // 参数未改变,无需重新登录,默认为已经登录成功
- }
- return false; // 需要重新登陆
- }
- }
shiro从Realm获取安全数据,也就是说SecurityManager要验证身份,它需要从Realm获取相应的用户进行比较以确定用户的身份是否合法;我们可以把Realm看作是DataSource,安全数据源。实现自定义Realm主要是继承AuthrizingRealm这个父类,重写doGetAuthrizationInfo和doGetAuthenticationInfo这两个方法,其中doGetAuthenticationInfo是用来验证用户合法性的,根据输入的用户信息从数据库中查出用户,根据用户情况抛出不同的异常。doGetAuthrizationInfo是对当前用的用户进行授权的。在这里我把角色写死了。在实际项目开发中应该是在表中做权限和角色的定义再从数据库中查出一个集合然后迭代授权。 具体实现代码如下:
- package com.gray.base.shiro;
- 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.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.springframework.beans.factory.annotation.Autowired;
- import com.gray.user.entity.User;
- import com.gray.user.service.impl.UserServiceImpl;
- public class ShiroDbRealm extends AuthorizingRealm {
- @Autowired
- private UserServiceImpl userService;
- public static final String SESSION_USER_KEY = "gray";
- /**
- * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用,负责在应用程序中决定用户的访问控制的方法
- */
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
- User user = (User) SecurityUtils.getSubject().getSession().getAttribute(ShiroDbRealm.SESSION_USER_KEY);
- SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
- info.addRole(user.getRole().trim());
- return info;
- }
- /**
- * 认证回调函数,登录信息和用户验证信息验证
- */
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(
- AuthenticationToken authcToken) throws AuthenticationException {
- // 把token转换成User对象
- User userLogin = tokenToUser((UsernamePasswordToken) authcToken);
- // 验证用户是否可以登录
- User ui = userService.doUserLogin(userLogin);
- if(ui == null)
- return null; // 异常处理,找不到数据
- // 设置session
- Session session = SecurityUtils.getSubject().getSession();
- session.setAttribute(ShiroDbRealm.SESSION_USER_KEY, ui);
- //当前 Realm 的 name
- String realmName = this.getName();
- //登陆的主要信息: 可以是一个实体类的对象, 但该实体类的对象一定是根据 token 的 username 查询得到的.
- // Object principal = ui.getUsername();
- Object principal = authcToken.getPrincipal();
- return new SimpleAuthenticationInfo(principal, userLogin.getPassword(), realmName);
- }
- private User tokenToUser(UsernamePasswordToken authcToken) {
- User user = new User();
- user.setUsername(authcToken.getUsername());
- user.setPassword(String.valueOf(authcToken.getPassword()));
- return user;
- }
- //一定要写getset方法
- public UserServiceImpl getUserService() {
- return userService;
- }
- public void setUserService(UserServiceImpl userService) {
- this.userService = userService;
- }
- }
具体的serviceImpl的实现我就不写出来了,数据库的查询我相信大家都会(默认你们都会)。随后如果有需要我会把整合代码放上来。
最后贴几张测试图:
登陆界面:
以管理员角色的账号登陆:
AdminPage:
以普通用户角色的账号登陆:
UserPage:
当你以普通用户登录却想访问adminpage时:
登陆失败:
最后登出点击logout返回登陆主界面。
以上就是整合Shiro的所有过程,当然在实际项目中不会那么简单但我觉得原理也是差不多的。学无止境,最后再次感谢网上提供资料的各位大神。