架构之路之spring+shiro的集成

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.
 */
@Component
public 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
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值