开端

好的,起因是我把最近做好的项目给一部分人进行了测试,发现大部分朋友都提出了同一个问题,你的系统权限管理是如何实现的。我只能尴尬的说一句,不好意思这部分还没开发。然而我也知道,其实对于一个项目来说,权限可以说是最主要的一部分。后端除了对权限进行处理,其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去,顺便把我的学习笔记分享给大家。

而对于权限控制的框架呢,听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的,不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架,可以用在各种场合,甚至非 Web 项目中,由于它的会话独立于容器,后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。

由于这部分内容比较多,我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服,所以我打算把这个内容分成几个部分:初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于如何利用我们之前学习的知识快速进行 Shiro 与前后端分离项目的整合。虽然说是快速,我还是会一步步都告诉大家,并没有想象中的那么快。

整合

那么之前结果两篇长文的讲解,大家应该已经初步了解了一些关于 Shiro 的知识,接下来我们开始快速整合 Shiro 和 SSM ,我只讲最关键的步骤,至于更多额外功能的拓展留给大家自行发掘。

下文的内容可能略微与我前面写的两篇文章有所不同,不过不影响食用。毕竟一边做一边写,总会发现之前的一些问题并找到更好的解决方法。

使用 Maven 管理 jar 包

Maven 是一个后端的包管理工具,可以使用写 XML 的形式快速从仓库寻找 jar 包。

使用阿里云仓库

众所周知,如果你从 github clone 一个项目,如果没有梯子下载 jar 包可能就要花几个小时的时间。还好阿里爸爸给我们提供了镜像仓库,几分钟就能解决 jar 包的问题。关于引用仓库可以在 maven 配置中修改:

<mirror>
  <id>aliyunmaven</id>
  <mirrorOf>*</mirrorOf>
  <name>阿里云公共仓库</name>
  <url>https://maven.aliyun.com/repository/public</url>
</mirror>

不过我还是推荐大家在自己的项目中加入仓库,灵活性更高:

<repositories>
    <repository>
        <id>alimaven</id>
        <name>aliyun maven</name>
        <url>https://maven.aliyun.com/repository/public</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

这里我就直接用 public 仓库,这个仓库整合了两个小仓库,东西更丰富些。releases 是线上版本,snapshots 是开发版本,都填 true 问题不大。

搜索并导入 jar 包

大家可以在上边这个网址内搜索自己要找的 jar 包,中心 central 仓库够大了,组织的话就是 Shiro 的网址,按结构写不要反了,然后输入文件名,点击搜索到的蓝色文件名后把 Maven 依赖这些代码复制进项目里即可。

截屏2020-08-01 下午2.09.48

像下面的一样,我们引入最新版本的 Shiro 相关的 jar 包。具体使用哪些包的话请大家去官网上查找资料,比如说官网里提示说 core 包需要 slf4j 的支持,再比如说你用了谷歌的 IOC 容器,那么需要使用 guice 包等等。

<!-- Shiro all -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-all</artifactId>
    <version>1.5.3</version>
</dependency>
<!-- Shiro核心包 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.3</version>
</dependency>
<!-- 添加Shiro web支持 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.5.3</version>
</dependency>
<!-- 添加Shiro Spring支持 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
</dependency>
<!-- 添加Shiro Spring Boot支持 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<!-- 添加日志包-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.9</version>
</dependency>

日志包和 Spring Boot 包都不是必要的,主要还是 shiro-all 和 shiro-core 这两个核心包。

权限五表的建立

请转到下文,翻到最后,把权限五表的 SQL 代码运行一下,完成用户、角色。权限、用户角色、角色权限五表的建立。我在这里贴出我的五张表,与下文中的略微不同,为了方便整合作出了一些调整。

MySQL建表

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
    `id` INT AUTO_INCREMENT NOT NULL,
    `usrName` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `usrPassword` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `usrNick` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `usrPhone` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `usrEmail` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `remark` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `lastLogin` BIGINT DEFAULT 0 NOT NULL,
    `utcCreate` BIGINT DEFAULT 0 NOT NULL,
    `utcModify` BIGINT DEFAULT 0 NOT NULL,
    `modifyBy` VARCHAR ( 255 ) DEFAULT '' NOT NULL,
    `valid` BIT (1) DEFAULT 1 NOT NULL,
PRIMARY KEY ( `id` )
) ENGINE=INNODB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
 `id` int(64) NOT NULL AUTO_INCREMENT,
 `description` varchar(32) DEFAULT NULL,
 `role` varchar(32) DEFAULT NULL,
 `remark` varchar( 255 ) DEFAULT NULL,
 `utcCreate` BIGINT DEFAULT 0,
 `utcModify` BIGINT DEFAULT 0,
 `modifyBy` VARCHAR ( 255 ) DEFAULT NULL,
 `valid` BIT (1) DEFAULT 1,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
 `id` int(64) NOT NULL AUTO_INCREMENT,
 `name` varchar(64) DEFAULT NULL,
 `permission` varchar(64) DEFAULT NULL,
 `url` varchar(64) DEFAULT NULL,
 `remark` varchar( 255 ) DEFAULT NULL,
 `utcCreate` BIGINT DEFAULT 0,
 `utcModify` BIGINT DEFAULT 0,
 `modifyBy` VARCHAR ( 255 ) DEFAULT NULL,
 `valid` BIT (1) DEFAULT 1,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
 `role` VARCHAR(255) NOT NULL,
 `user_name` VARCHAR(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
 `role` VARCHAR(255) NOT NULL COMMENT '角色名',
 `permission` VARCHAR(255) NOT NULL COMMENT '权限名'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

主要的不同在于两张关联表中我都是假定某个字段不能修改,然后把它当成关联键进行操作。如果直接用用户、角色、权限表的 id 主键的话确实更方便更合理,但是我还没正式学过 SQL ,只会简单的增删改查。前面讲到的正统方法涉及 SQL 中 LEFT JOIN 等操作,比较复杂,各位要是学过 SQL 推荐用主键关联。

后端五表一条龙

后端对于这五张表的操作主要在 POJO 层、Mapper 层、Mybatis 层、Service 层和 Controller 层。就和你的项目系统中任何一个实体类一样,方方面面都需要涉及。那么我相信大家既然看到这里,已经会写接口会写 Mybatis,所以怎么写就不讲了,贴一些重点的内容。

POJO

POJO 层的实体类属性需要严格按照之前 MySQL 中的情况来,具体见下表:

20170930200408361

就举一个 Permission 的权限实体作参考,使用了 Lombok。

@Getter
@Setter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Permission extends BaseEntity {
    private static final long serialVersionUID = 8482551052982019660L;
    private Integer id;
    private String name;
    private String permission;
    private String url;
}

创建时间、修改时间等公共属性都放在 BaseEntity 中。就只要这么写一个实体类就完成了,Lombok 注解实现非常简单。同样对用户和角色作如上操作即可。

Mapper、Mybatis

这块关联性大一些,就放在一起写了。主要就是在 Mapper 中写接口,在 XML 的配置文件里写对应方法的 Mybatis SQL 语句。增删改查不多说,由于我们的 Shiro 在认证时需要查询用户、查询用户角色和查询角色权限这三个数据库操作,因此我只写这三个,其他自行完成。

@Repository
public interface UserMapper {
    User selectByName(@Param("usrName") String usrName);
}
@Repository
public interface RoleMapper {
    Set<String> findRoleByUserName(@Param("usrName") String usrName);
}
@Repository
public interface PermissionMapper {
    Set<String> findPermissionByRole(@Param("role") String role);
}

注意一下根据官方给出的案例,后边两个操作需要返回 Set 集合,我没试过 List 可不可行,既然例子是 Set 咱们也用 Set。接下来写 XML(我写在一起了,实际需要分三个 XML 写):

<select id="selectByName" parameterType="String" resultType="User">
    SELECT *
    FROM sys_user
    WHERE usrName = #{usrName}
</select>
<select id="findRoleByUserName" parameterType="String" resultType="String">
    SELECT role
    FROM sys_user_role
    WHERE user_name = #{usrName}
</select>
<select id="findPermissionByRole" parameterType="String" resultType="String">
    SELECT permission
    FROM sys_role_permission
    WHERE role = #{role}
</select>

需要注意的是,如果在 Spring 配置中没有为 Mybatis 对应的 POJO 配置 type-aliases,需要写全包名。而像 String 这种基本包装类简写会报错但不影响程序运行,这是 IDEA 的一个小 bug。千万别忘了写增删改查等等其他操作,我们接着往下。

Service

Service 需要对数据库的原子操作开始组合,由于涉及接口和实现类,我这里只举一个例子:

public interface PermissionService {
  Set<String> findPermissionByRole(String paramRole);
}

@Service
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
public class PermissionServiceImpl implements PermissionService {
    @Resource
    private PermissionMapper permissionMapper;
    @Override
    public Set<String> findPermissionByRole(String paramRole) {
        return permissionMapper.findPermissionByRole(paramRole);
    }
}

注解主要是告诉 Spring 这是一个 Service ,交给它管理,然后设置一下事物传播属性、报错回滚类型、事物隔离级别等。其他基本业务需求也请自行组装。

Controller

终于到了最后一层,这一层就和前面的不太一样了。角色和权限的 Service 接口只在 Shiro 配置的时候用到,所以不需要写其他的。除非你的系统中涉及权限和角色的增删改查和分配才需要写,当然一般来说都是有这个需求的。我依旧是只举一个例子:

@CrossOrigin
@RestController()
@RequestMapping("/permission/")
public class PermissionController extends BaseController {
    @Resource(name = "permissionServiceImpl")
    private PermissionServiceImpl permissionServiceImpl;
    @GetMapping(value = "/findPermissionByRole")
    @ResponseBody
    public Response<Set<String>> findPermissionByRole(@RequestParam String role) {
        Set<String> results = permissionServiceImpl.findPermissionByRole(role);
        if (results.size() != 0) {
            return getSuccessResult(results);
        }
        return getFailResult(404, "MEssage not found!");
    }

这里的跨域注解只适用于项目初期,结合了 Vue 特别是整合了 Shiro 之后,跨域问题已经不是这个注解能够解决的了,我会在后面讲到。对于三个 Controller 的功能请自行完成。

至此,对于我们来讲项目基本所需的新内容已经完成,接下来就是要进行 Shiro 的配置来实现权限的自动管理。

配置Shiro

开始配置 Shiro,我要先提跨域问题。在项目初期,跨域是不存在的,因为只有后端的程序在跑,直接用 Postman 测试不会出问题。在我们整合了 Vue 之后,开始出现跨域问题。由于本地测试时 Vue 其实是跑在 Node.js 的,前后端是两个不同的端口号。但是这是的跨域问题也不是特别难解决,设置一个全局跨域处理即可。说的简单其实我第一次碰到的时候捉摸了好几个周才解决。

但是当我们整合 Shiro,就会多出好多问题。讲个容易理解的问题,在访问网页的时候并不是一次连通的,又是会遇到多次“握手”,浏览器先发送一个 OPTIONS 类型的请求(PreFlight)给后端,这种叫做预检请求,以检测实际请求是否可以被服务器所接收。那么问题就来了,既然我们设置了权限管理,用户在登录系统前肯定是不被允许有其他权限的,那么这个所谓的预检请求,就会被 Shiro 拦截。在 Shiro 中我们只能对指定接口指定 URL 的内容进行拦截或放行,但是对于预检请求就无能为力。

这只是一个小例子,实际开发中还会遇到其他各种问题。所以我们先从跨域处理开始,以应对之后遇到的问题。

创建工具类解决跨域

对跨域忍无可忍,终于集合各路奇技,写了一个工具类解决一切跨域问题。

public class CorsUtil {
    private static final String OPTIONS_FOR_REQUEST = "OPTIONS";
    public static void setResponseHeader(HttpServletResponse response, HttpServletRequest request) {
        // 设置编码格式
        response.setContentType("text/html;charset=UTF-8");
        // 允许哪些Origin发起跨域请求,nginx下正常
        response.setHeader( "Access-Control-Allow-Origin", request.getHeader("Origin"));
        // 允许请求的方法
        response.setHeader( "Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE" );
        // 多少秒内,不需要再发送预检验请求,可以缓存该结果
        response.setHeader( "Access-Control-Max-Age", "86400" );
        // 表明它允许跨域请求包含xxx头
        response.setHeader( "Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With," +
                "If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type," +
                "X-E4M-With,userId,token, Accept, Authorization" );
        // 允许浏览器携带用户身份信息(cookie)
        response.setHeader( "Access-Control-Allow-Credentials", "true" );
        response.setHeader("XDomainRequestAllowed","1");
        // 允许OPTIONS请求直接通过
        if (OPTIONS_FOR_REQUEST.equals((request).getMethod().toUpperCase())) {
            response.setStatus(HttpServletResponse.SC_OK);
        }
    }
}

可以看到此类中,编码设置了;对源头地址直接采用 get 的方法不需要每次都重新修改了;请求方法接受 POST、GET、OPTIONS 和 DELETE;请求中也允许了各种类型奇奇怪怪的请求头,应有尽有;最后还设置了允许携带 Cookie 信息,允许同步 Session 信息等。方法的参数是 HttpServlet 中的 Response 和 Request,对 Response 进行处理后返回。基本上以后遇到什么需要跨域的处理,直接在需要的地方调用这个方法即可,比我之前百度到的什么配置全局拦截器好用多了。

创建登录拦截器

既然写好的跨域根据类,接下来我们重写一下 Shiro 的跨域过滤器。

@Component
@WebFilter(urlPatterns = "/*",filterName = "shiroCrossFilter")
public class ShiroLoginFilter  implements Filter {
    private FilterConfig config = null;
    @Override
    public void init(FilterConfig config) throws ServletException {
        this.config = config;
    }
    @Override
    public void destroy() {
        this.config = null;
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        CorsUtil.setResponseHeader(response, request);
        filterChain.doFilter( servletRequest, response );
    }
}

最主要的是 @WebFilter注解,名字千万不能错,要完全一致。然后重写一下三个方法,在 doFilter( ) 方法中使用我们刚刚写的跨域工具类处理。

创建全局异常处理器

针对登陆账户和操作权限时因某种因素操作失败,Shiro 会直接在控制台抛出异常,我们设置一个全局处理器找到这些异常并返回一些容易辨认的中文提示,也方便前端进行提示。

@RestControllerAdvice
public class GlobalExceptionHandle {
    @ExceptionHandler(ShiroException.class)
    public String doHandleShiroException(ShiroException se, Model model) {
        se.printStackTrace();
        if(se instanceof UnknownAccountException) {
            return "该账户不存在";
        } else if (se instanceof LockedAccountException) {
            return "该账户已锁定";
        } else if (se instanceof IncorrectCredentialsException) {
            return "密码错误请重试";
        } else if (se instanceof AuthorizationException) {
            return "没有相应权限";
        } else {
            return "操作失败请重试";
        }
    }
}

创建自定义会话管理器

public class CustomSessionManager extends DefaultWebSessionManager {
    private static final Logger logger = LoggerFactory.getLogger(CustomSessionManager.class);
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    public CustomSessionManager() {
        super();
        setGlobalSessionTimeout(DEFAULT_GLOBAL_SESSION_TIMEOUT * 48);
    }
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        if (!StringUtils.isEmpty(sessionId)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        } else {
            return super.getSessionId(request, response);
        }
    }
}

这部分照抄就可以了,没什么好讲的。

创建授权拦截器

对于访问某个资源,会出现两种情况,一种就是你有权限可以访问,一种就是你没有权限被 Shiro 拦截,在这里也会涉及跨域问题,所以我们继承 UserFilter (根据你的默认网页拦截规则选择继承的类),重写通过和拦截两个方法,首先给大家 Shiro 默认拦截器类对应的名称,再给代码。

名称
authcFormAuthenticationFilter
authcBasicBasicHttpAuthenticationFilter
logoutLogoutFilter
userUserFilter
anonAnonymousFilter
rolesRolesAuthorizationFilter
permsPermissionsAuthorizationFilter
portPortFilter
restHttpMethodPermissionFilter
sslSslFilter
noSessionCreationNoSessionCreationFilter

每个拦截器的作用我在前文都讲过,不赘述了。

public class CorsAuthenticationFilter extends UserFilter {
    private static final String OPTIONS_FOR_REQUEST = "OPTIONS";
    public CorsAuthenticationFilter() {
        super();
    }
    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (OPTIONS_FOR_REQUEST.equals(((HttpServletRequest) request).getMethod().toUpperCase())) {
            return true;
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse res = (HttpServletResponse)response;
        HttpServletRequest req = (HttpServletRequest)request;
        CorsUtil.setResponseHeader(res, req);
        res.setStatus(HttpServletResponse.SC_OK);
        PrintWriter writer = res.getWriter();
        Map<String, Object> map= new HashMap<>(16);
        map.put("success", false);
        map.put("code", 702);
        map.put("message", "用户未登录");
        map.put("timestamp", DateUtil.currentSecond());
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

当用户认证成功,只需要判断请求是否是 OPTIONS 类型,如果是直接放行。当用户认证失败,用户就不具有权限访问,需要调用我们的跨域工具进行处理,并且告诉控制台用户未登录,返回 false 进行拦截。

创建安全实体数据源

接下来的两块是最重要的,在之前的文章中也多次讲到过,这里再次演示一遍。

public class MyShiroRealm extends AuthorizingRealm {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyShiroRealm.class);
    @Resource(name = "roleServiceImpl")
    private RoleService roleService;
    @Resource(name = "permissionServiceImpl")
    private PermissionService permissionService;
    @Resource(name = "userServiceImpl")
    private UserService userService;
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String userName = token.getUsername();
        User user = userService.selectByName(userName);
        if (user == null) {
            throw new UnknownAccountException("Message not found");
        } else if (!user.getValid()) {
            throw new LockedAccountException("Account locked");
        }
        return new SimpleAuthenticationInfo(
            user.getUsrName(),
            user.getUsrPassword(),
            ByteSource.Util.bytes(userName),
            getName()
        );
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String userName = (String)principalCollection.getPrimaryPrincipal();
        if (userName == null) {
            LOGGER.error("授权失败,用户信息为空!");
            return null;
        }
        try {
            Set<String> roles = roleService.findRoleByUserName(userName);
            simpleAuthorizationInfo.addRoles(roles);
            for (String role : roles) {
                Set<String> permissions = permissionService.findPermissionByRole(role);
                simpleAuthorizationInfo.addStringPermissions(permissions);
            }
            return simpleAuthorizationInfo;
        } catch (Exception e) {
            LOGGER.error("授权失败,系统内部错误!");
        }
        return simpleAuthorizationInfo;
    }
}

需要注意的是按照原则,应当注入接口,使用@Resource注解和实现类的名字进行注入。

用户验证使用 UserService 查询数据库中的用户信息并验证;用户授权使用 PermissionService 和 RoleService,首先从数据库读取用户角色交给 Shiro 管理。对于每个用户所对应的角色都从数据库再读取角色对应权限,由于 Set 集合有去重性,会得到唯一的 permission 集合,将其交给 Shiro 管理。具体的就不多说啦,见前两篇文章吧。

创建配置

之前写了这么多杂七杂八的,其实都是打地基,接下来要把他组合在 Shiro 配置中让其生效。

@Configuration
public class ShiroConfiguration {
    private final Log logger = LogFactory.getLog(ShiroConfiguration.class);
    @Bean
    public MyShiroRealm myShiroRealm(){
        // 生成之前配置的数据源
        return new MyShiroRealm();
    }
    @Bean
    public CacheManager cacheManager() {
        // 生成缓存管理器
        return new MemoryConstrainedCacheManager();
    }
    @Bean
    public SessionManager sessionManager() {
        // 生成之前配置的会话管理器
        return new CustomSessionManager();
    }
    @Bean
    public CorsAuthenticationFilter corsAuthenticationFilter() {
        // 生成之前配置的授权拦截器
        return new CorsAuthenticationFilter();
    }
    @Bean
    public DefaultWebSecurityManager securityManager(){
        // 生成默认安全管理器后将上文中配置全部加入其中
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        securityManager.setCacheManager(cacheManager());
        securityManager.setRememberMeManager(rememberMeManager());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    // 这部分的含义见之前的文章
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/user/login");
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/favicon.ico","anon");
        filterChainDefinitionMap.put("/user/login","anon");
        filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }
    /**
     * SpringShiroFilter首先注册到spring容器
     * 然后被包装成FilterRegistrationBean
     * 最后通过FilterRegistrationBean注册到servlet容器
     */
    @Bean
    public FilterRegistrationBean delegatingFilterProxy(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        proxy.setTargetFilterLifecycle(true);
        proxy.setTargetBeanName("shiroFilter");
        filterRegistrationBean.setFilter(proxy);
        return filterRegistrationBean;
    }

    @Bean
    public SimpleCookie rememberMeCookie(){
        // 配置rememberMe Cookie
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setMaxAge(24 * 60 * 60);
        simpleCookie.setHttpOnly(false);
        return simpleCookie;
    }

    @Bean
    public CookieRememberMeManager rememberMeManager(){
        // 将Cookie交给管理器并加密
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        return cookieRememberMeManager;
    }

    // 后面这仨方法直接照抄即可,配置生命周期和代理,否则Shiro无法生效
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    @ConditionalOnMissingBean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

至此配置就算是全部完成了,接下来只需要在 Controller 层设置所需权限。

注解标记权限

@RestController()
@CrossOrigin
@RequestMapping("/user/")
public class UserController extends BaseController {
    @Resource(name = "userServiceImpl")
    private UserService userService;
    @RequiresRoles("admin")
    @RequiresPermissions("user:selectAll")
    @GetMapping(value = "/selectAll")
    @ResponseBody
    public Response<List<User>> selectAll() {
        List<User> result = userService.selectAll();
        if (result.size() != 0) {
            return getSuccessResult(result);
        }
        return getFailResult(404, "Message not found!");
    }
}

比如我的用户 Controller 中的查询方法,同时使用@RequiresRoles@RequiresPermissions表示用户不仅需要 admin 角色,而且还需要 user:selectAll 权限才能够使用此 api 接口,缺一不可。换句话来说,如果我给其他角色也分配了这个权限,他们依旧不能使用此方法,admin 用户被删除该权限后也将无法使用此方法。

除此之外我再贴一个登陆方法和一个退出登录方法,其他都和查询方法类似。

@PostMapping(value = "/login")
@ResponseBody
public Response<User> userLogin(@RequestBody User sysUser) {
    User result;
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsrName(), sysUser.getUsrPassword());
    token.setRememberMe(true);
    try {
        subject.login(token);
        result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword());
        session.setAttribute("user", subject);
    } catch (UnknownAccountException e) {
        return getFailResult(404, "Message not found");
    } catch (IncorrectCredentialsException e) {
        return getFailResult(412, "Incorrect message");
    } catch (LockedAccountException e) {
        return getFailResult(401, "Account locked");
    } catch (Exception e) {
        e.printStackTrace();
        return getFailResult(408, "Unknown error");
    }
    return getSuccessResult(result);
}
@GetMapping(value = "/logout")
@ResponseBody
public Response<String> userLogout() {
    CorsUtil.setResponseHeader(response, request);
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.logout();
    } catch (SessionException e) {
        e.printStackTrace();
        return getFailResult(408, e.toString());
    }
    return getSuccessResult("logout");
}

这部分之前也讲过,这里不说了,由于整合 Shiro 是在涉及了大量的内容,如果全部解释一遍今天这篇又得拆成很多篇来讲,我之所以分三篇写就是为了让这一篇的内容分散到其他两篇中去,有需要的同学可以回去巩固一下基础知识。

最终测试

差不多可以收工了,我们最后再使用 Postman 测试一下接口看看是什么效果。

截屏2020-08-23 下午7.28.01

直接调查询所有用户的接口,发现提示“用户未登录”。

截屏2020-08-23 下午7.30.32

接下来调用登录接口并传数据库中已经有的用户参数,发现登录成功。

截屏2020-08-23 下午7.32.15

在登录之后重新调用查询接口,发现现在已经拥有看权限,可以点开 Cookie 查看,也是我们在后端设置过的 rememberMe,Headers 中也有我们设置的跨域处理内容。

那么至此,Shiro 整合 SSM 项目就大功告成了,你可以在后端拓展一些关于权限方面的业务,在前端也做一些权限控制的内容,使项目更加完善。

总结

这篇文章解释性的话并不多,主要都是代码。从头到尾,贴出了配置 Shiro 并整合到项目中的过程。准确的来说,如果想要权限的功能完完全全的配置成功,这些代码都是必不可少的。因为其中还涉及跨域问题的处理等在实际操作中会遇到的问题,许多问题都是我在操作中遇到并解决的,其他一些博客中则并没有讲到。希望这篇文章对你有所帮助,尤其是也在开发前后端分离项目并且遇到很多问题的同学。

接下来我打算写一篇前后端项目整合 Swagger 的文章,毕竟 api 接口对于前后端分离的项目可以说是两个人沟通的桥梁,非常容易在这一块出问题,学会使用 Swagger 不仅能减少问题的产生,也能优化我们的后台管理系统,丰富其内容。如果有什么建议,欢迎评论交流。

最后修改:2022 年 05 月 27 日
随意