架构之路之spring+shiro的集成

来源:互联网 发布:解压for mac 编辑:程序博客网 时间:2024/05/01 07:32

1.前言

1.1 shiro介绍


Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

1.2使用api

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

2.集成项目

2.1 依赖

<!-- shiro -->        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-core</artifactId>            <version>1.2.3</version>        </dependency>        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-web</artifactId>            <version>1.2.3</version>        </dependency>        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-spring</artifactId>            <version>1.2.3</version>        </dependency>

2.2 shiro-context.xml

<?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.0.xsd">    <!--自定义权限认证-->    <bean id="authRealm" class="com.tl.skyLine.shiro.AuthRealm">        <!--自定义密码加密算法  -->        <!--<property name="credentialsMatcher" ref="passwordMatcher"/>-->        <!--<property name="roleDao" ref="roleDao"/>-->        <!--<property name="permissionDao" ref="permissionDao"/>-->    </bean>    <!-- 设置密码加密策略 md5hash -->    <!--<bean id="passwordMatcher" class="com.fyh.www.shiro.CustomCredentialsMatcher"/>-->    <!-- 配置权限管理器(核心) -->    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">        <!-- 我们自定义的realm -->        <property name="realm" ref="authRealm"/>        <!-- 缓存管理器 -->        <property name="cacheManager" ref="cacheManager"/>    </bean>    <!-- 此bean要被web.xml引用,和web.shiroFilter -->    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">        <!-- 权限管理器 -->        <property name="securityManager" ref="securityManager"/>        <!-- 登录地址 -->        <property name="loginUrl" value="/pages/login.jsp"/>        <!-- 登录后跳转到业务页面 -->        <property name="successUrl" value="/pages/home.jsp"/>        <!-- 错误页面 -->        <property name="unauthorizedUrl" value="/pages/unauthorized.jsp"/>        <!-- 权限配置 -->        <property name="filterChainDefinitions">            <value>                <!-- 匿名登录请求 -->                /public/** = anon                <!-- **代表任意子目录 -->                /static/**=anon                <!-- 需要权限为edit的用户才能访问此请求-->                /user/edit=perms[user:edit]                <!-- 需要管理员角色才能访问此页面 -->                <!--/user/edit=roles[admin]-->                <!--拦截非静态资源的所有请求-->                /** = authc                <!--authc:确保已认证的用户发送的请求才能通过(若未认证,则跳转到登录页面)-->            </value>        </property>    </bean>    <!-- 用户授权/认证信息Cache, 采用EhCache  缓存 -->    <!--<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">-->    <!--<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>-->    <!--</bean>-->    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>    <!-- 安全管理器 -->    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">        <property name="securityManager" ref="securityManager"/>    </bean>    <!--启用Shiro相关的注解-->    <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></beans>

2.3 导入spring-context.xml和springmvc-context.xml中

<import resource="shiro-context.xml"/>

这边对之前的配置要做一些修改,涉及到spring父子容器的查看范围的问题,参考下一篇文章

点击打开链接

修改的spring-context.xml:

 <context:component-scan base-package="com.tl.skyLine">        <!-- 排除不扫描的,controller放在子容器springmvc的配置文件中 -->        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>    </context:component-scan>

修改的springmvc-context.xml:

<!--只在springmvc容器中配置controller的扫描-->    <context:component-scan base-package="com.tl.skyLine" use-default-filters="false">        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>    </context:component-scan>

2.4 web.xml

<filter>        <filter-name>shiroFilter</filter-name>        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>    </filter>    <filter-mapping>        <filter-name>shiroFilter</filter-name>        <url-pattern>/*</url-pattern>    </filter-mapping>

这个shiroFilter与shiro-context.xml中的<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">对应。

2.5 自定义AuthRealm

package com.tl.skyLine.shiro;import com.tl.skyLine.model.User;import com.tl.skyLine.repository.PermissionDao;import com.tl.skyLine.repository.RoleDao;import com.tl.skyLine.repository.UserDao;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 org.springframework.stereotype.Component;import java.util.stream.Collectors;/** * Created by tl on 17/2/20. */@Componentpublic class AuthRealm extends AuthorizingRealm {    @Autowired    private RoleDao roleDao;    @Autowired    private PermissionDao permissionDao;    @Autowired    private UserDao userDao;    /**     * 用来为当前登陆成功的用户授予权限和角色(已经登陆成功了)     */    @Override    protected AuthorizationInfo doGetAuthorizationInfo(            PrincipalCollection principals) {        //获取用户名        //String username = (String) principals.getPrimaryPrincipal();        //获取当前用户        User user = (User) principals.fromRealm(getName()).iterator().next();        //得到权限字符串        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();        info.addRoles(roleDao.getRoles(user.getId())                .stream().map(role -> role.getName()).collect(Collectors.toList()));        info.addStringPermissions(permissionDao.getPermissionByUser(user.getId())                .stream().map(permission -> permission.getName()).collect(Collectors.toList()));        return info;    }    /**     * 用来验证当前登录的用户,获取认证信息     */    @Override    protected AuthenticationInfo doGetAuthenticationInfo(            AuthenticationToken authcToken) throws AuthenticationException {        UsernamePasswordToken upToken = (UsernamePasswordToken) authcToken;        User user = userDao.findOneByUsername(upToken.getUsername());        if (user == null) {            return null;        } else {            AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());            return info;        }    }}

在认证、授权内部实现机制中都有提到,最终处理都将交给Realm进行处理。 因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。 可以说,Realm是专用于安全框架的DAO.

shiro登陆成功,再次访问url的时候会请求doGetAuthorizationInfo时候会将登陆用户对应的角色,权限全部查到,然后set到SimpleAuthorizationInfo实例对象info中,保存的分别是role和permission对象的name字段,


这个name子段跟shiro-context.xml中的shiroFilterbean对象的filterChainDefinitions属性对应:


2.6 角色权限类的结构


我这边是用mongodb数据库,没有外键关联约束,就是基本的用户,角色,权限,然后外加两个中间表,我就不一一贴出了。

2.7 repository

由于非关系数据库的约束性,这边做表关联查询没有关系型数据库那么简单,这个对整体框架没有影响,我把这个代码贴一下:

RoleDaoImpl:

package com.tl.skyLine.repository.impl;import com.tl.skyLine.model.Role;import com.tl.skyLine.model.UserRole;import com.tl.skyLine.repository.RoleDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * RoleDaoImpl * Created by tl on 17/2/13. */@Component("roleDao")public class RoleDaoImpl implements RoleDao {    @Autowired    private MongoTemplate mongoTemplate;    @Override    public Role findOne(String roleId) {        return this.mongoTemplate.findOne(new Query().addCriteria(Criteria.where("id").is(roleId)), Role.class);    }    @Override    public List<Role> getRoles(String userId) {        Query query = new Query();        query.addCriteria(Criteria.where("userId").is(userId));        List<UserRole> userRoles = this.mongoTemplate.find(query, UserRole.class);        List<Role> roles = new ArrayList<Role>();        userRoles.stream().forEach(userRole -> {            roles.add(this.findOne(userRole.getRoleId()));        });        return roles;    }    @Override    public void store(Role role) {        this.mongoTemplate.save(role);    }    @Override    public void store(UserRole userRole) {        this.mongoTemplate.save(userRole);    }    @Override    public Role findOneByName(String name) {        return this.mongoTemplate.findOne(                new Query().addCriteria(Criteria.where("name").is(name))                , Role.class);    }}

PermissionImpl:

package com.tl.skyLine.repository.impl;import com.tl.skyLine.model.Permission;import com.tl.skyLine.model.RolePermission;import com.tl.skyLine.repository.PermissionDao;import com.tl.skyLine.repository.RoleDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * PermissionDaoImpl * Created by tl on 17/2/13. */@Component("permissionDao")public class PermissionDaoImpl implements PermissionDao {    @Autowired    private MongoTemplate mongoTemplate;    @Autowired    private RoleDao roleDao;    @Override    public Permission findOne(String permissionId) {        return this.mongoTemplate.findOne(new Query().addCriteria(Criteria.where("id").is(permissionId)), Permission.class);    }    @Override    public List<Permission> getPermissionByRole(String roleId) {        Query query = new Query();        query.addCriteria(Criteria.where("roleId").is(roleId));        List<RolePermission> rolePermissions = this.mongoTemplate.find(query, RolePermission.class);        List<Permission> permissions = new ArrayList<Permission>();        rolePermissions.stream().forEach(rolePermission -> {            permissions.add(this.findOne(rolePermission.getPermissionId()));        });        return permissions;    }    @Override    public List<Permission> getPermissionByUser(String userId) {        List<Permission> permissions = new ArrayList<Permission>();        roleDao.getRoles(userId).stream().forEach(role -> {            permissions.addAll(this.getPermissionByRole(role.getId()));        });        return permissions;    }    @Override    public void store(Permission permission) {        this.mongoTemplate.save(permission);    }    @Override    public void store(RolePermission rolePermission) {        this.mongoTemplate.save(rolePermission);    }    @Override    public Permission findOneByName(String name) {        return this.mongoTemplate.findOne(                new Query().addCriteria(Criteria.where("name").is(name))                , Permission.class);    }}

2.8 单元测试增加测试数据

    /**     * 关联用户角色权限     */    @Test    public void testShiro() {        Role role = new Role();        role.setName("admin");        role.setDescription("管理员权限");        roleDao.store(role);        UserRole userRole = new UserRole();        userRole.setRoleId(role.getId());        userRole.setUserId(userDao.findOneByUsername("admin").getId());        roleDao.store(userRole);        Permission permission = new Permission();        permission.setName("edit");        permission.setDescription("编辑权限");        permissionDao.store(permission);        RolePermission rolePermission = new RolePermission();        rolePermission.setRoleId(role.getId());        rolePermission.setPermissionId(permission.getId());        permissionDao.store(rolePermission);    }

2.9 controller类和jsp

PublicController:

/** * PublicController * Created by tl on 17/2/13. */@Controller@RequestMapping("/public")public class PublicController {    @Autowired    private UserDao userDao;    //用户登录    @RequestMapping("/login")    public String login(User user, HttpServletRequest request) {        Subject subject = SecurityUtils.getSubject();        userDao.findAll();        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());        try {            subject.login(token);//会跳到我们自定义的realm中            request.getSession().setAttribute("user", user);            return "success";        } catch (Exception e) {            e.printStackTrace();            request.getSession().setAttribute("user", user);            request.setAttribute("error", "用户名或密码错误!");            return "login";        }    }    @RequestMapping("/logout")    public String logout(HttpServletRequest request) {        request.getSession().invalidate();        return "index";    }}

login.jsp

<!DOCTYPE html><%@ page language="java" contentType="text/html; charset=GB2312" %><html lang="en"><head>    <title>登录页面</title></head><body><%    String path = request.getContextPath();// 获得项目完全路径(假设你的项目叫skyLine,那么获得到的地址就是http://localhost:8080/skyLine/):    String basePath = request.getScheme() + "://"            + request.getServerName() + ":" + request.getServerPort()            + path + "/";%><form action="${basePath}/public/login" method="post">    <span>用户名:</span><input type="text" name="username"/><br>    <span>密码:</span><input type="password" name="password"/><br>    <input type="submit" value="登陆">${error}</form></body></html>

success.jsp

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %><!DOCTYPE html><%@ page language="java" contentType="text/html; charset=GB2312" %><html lang="en"><head>    <title>success</title></head><body><p>登陆成功!欢迎你${user.username}</p><a href="${pageContext.request.contextPath}/user/list">进入用户list页面</a><shiro:hasPermission name="user:edit">    <p>你有权限看到此处!</p></shiro:hasPermission></body></html>

上面的<shiro:hasPermission name="user:edit">注解,下节再讲

点击打开链接

3.开始测试shiro权限

     首先项目启动之后,不再运行web.xml这行代码<welcome-file-list>,而是直接被shiroFilter过滤,执行shiro-context.xml配置文件中的<property name="loginUrl" value="/pages/login.jsp"/>,进入登录页面,然后输入登录信息,进入publicController中的login方法,执行到subject.login(token);时会跳转到我们自定义的realm中进行用户名和密码校验,成功跳转success.jsp,失败重新返回login.jsp页面,同时显示报错信息!

    下面针对shiro-contex.xml中的url设置,访问http://localhost:8080/user/edit,提示访问成功,如果将配置文件的/user/edit=perms[user:edit]中的use:edit改成use:edit,重新启动,再次访问则会报错,提示没有访问权限


   但是,上面的配置有一个局限性,就是每次在配置文件里面加url权限,会很麻烦,每次都要重启,而且shiro权限是细粒化的这种加配置的方法会产生很多代码,是有意下文我们将介绍通过注解在controller和jsp中如何实现权限细粒化!

 

1 0