springboot 整合 shiro

Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供。

1.准备权限相关数据库表

主要涉及到七张表:用户表,角色表,权限表(菜单下的按钮,链接等的访问权限),菜单表(系统的功能),用户-角色表(用户和角色是多对多的),角色-权限表(角色和权限是多对多的),菜单权限表。表结构建立的sql语句如下:

create table T_SYS_MENU
(
  id          VARCHAR2(32) not null,
  menu_code   VARCHAR2(64) not null,
  menu_name   VARCHAR2(64) not null,
  menu_level  INTEGER default 0 not null,
  order_no    INTEGER default 0 not null,
  state       INTEGER not null,
  menu_url    VARCHAR2(200),
  leaf        NUMBER(1),
  parent_code VARCHAR2(64),
  create_time DATE not null,
  update_time DATE not null,
  icon        VARCHAR2(32)
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_MENU
  is '系统-菜单表';
comment on column T_SYS_MENU.state
  is ' 1-激活    2-禁用';
comment on column T_SYS_MENU.menu_url
  is '菜单关联的路径';
comment on column T_SYS_MENU.leaf
  is '是否是叶子菜单  1-true  0-false';
comment on column T_SYS_MENU.parent_code
  is '父菜单';
comment on column T_SYS_MENU.icon
  is '图标';
alter table T_SYS_MENU
  add constraint PK_SYS_MENU primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

create table T_SYS_MENU_PER_REL
(
  id          VARCHAR2(32) not null,
  menu_id     VARCHAR2(32) not null,
  per_id      VARCHAR2(32) not null,
  create_by   VARCHAR2(32),
  create_time DATE
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_MENU_PER_REL
  is '系统-菜单权限关联表';
alter table T_SYS_MENU_PER_REL
  add constraint PK_SYS_MENU_PER_REL primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

create table T_SYS_PERMISSION
(
  id          VARCHAR2(32) not null,
  per_code    VARCHAR2(64) not null,
  per_name    VARCHAR2(64) not null,
  state       INTEGER not null,
  descs       VARCHAR2(100),
  create_time DATE not null,
  update_time DATE not null
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_PERMISSION
  is '系统-权限表';
comment on column T_SYS_PERMISSION.per_code
  is '权限编号';
comment on column T_SYS_PERMISSION.per_name
  is '权限名称';
comment on column T_SYS_PERMISSION.state
  is ' 1-激活  2-禁用';
comment on column T_SYS_PERMISSION.descs
  is '描述';
alter table T_SYS_PERMISSION
  add constraint PK_SYS_PERMISSION primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

create table T_SYS_ROLE
(
  id          VARCHAR2(32) not null,
  role_name   VARCHAR2(64) not null,
  descs       VARCHAR2(500),
  create_by   VARCHAR2(32) not null,
  create_time DATE not null,
  update_by   VARCHAR2(32) not null,
  update_time DATE not null,
  state       INTEGER not null
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_ROLE
  is '系统-角色表';
comment on column T_SYS_ROLE.id
  is 'pk';
comment on column T_SYS_ROLE.role_name
  is '角色名称';
comment on column T_SYS_ROLE.descs
  is '描述';
comment on column T_SYS_ROLE.create_by
  is '创建人ID';
comment on column T_SYS_ROLE.create_time
  is '创建时间';
comment on column T_SYS_ROLE.update_by
  is '修改人ID';
comment on column T_SYS_ROLE.update_time
  is '更新时间';
comment on column T_SYS_ROLE.state
  is '1-激活  2-禁用';
alter table T_SYS_ROLE
  add constraint PK_T_SYS_ROLE primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

create table T_SYS_ROLE_PER_REL
(
  id          VARCHAR2(32) not null,
  role_id     VARCHAR2(32) not null,
  per_id      VARCHAR2(32) not null,
  create_by   VARCHAR2(32) not null,
  create_time DATE not null
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_ROLE_PER_REL
  is '系统 - 角色权限关联表';
alter table T_SYS_ROLE_PER_REL
  add constraint PK_T_SYS_ROLE_MODULE_REL primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

create table T_SYS_USER
(
  id           VARCHAR2(32) not null,
  login_name   VARCHAR2(24) not null,
  user_name    VARCHAR2(72) not null,
  passwd       VARCHAR2(32) not null,
  org_code     VARCHAR2(32),
  phone        VARCHAR2(64),
  state        INTEGER default 1 not null,
  user_type    INTEGER not null,
  created_by   VARCHAR2(32) not null,
  created_date DATE not null,
  updated_by   VARCHAR2(32) not null,
  updated_date DATE not null
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_USER
  is '系统-用户信息';
comment on column T_SYS_USER.id
  is '用户id';
comment on column T_SYS_USER.login_name
  is '登录账号';
comment on column T_SYS_USER.user_name
  is '用户名称';
comment on column T_SYS_USER.passwd
  is '密码';
comment on column T_SYS_USER.phone
  is '联系电话';
comment on column T_SYS_USER.state
  is ' 1:激活  2:冻结   3:刪除';
comment on column T_SYS_USER.user_type
  is '用户类型  1:超级管理员   2:系统用户';
comment on column T_SYS_USER.created_by
  is '创建人ID';
comment on column T_SYS_USER.created_date
  is '创建时间';
comment on column T_SYS_USER.updated_by
  is '修改人ID';
comment on column T_SYS_USER.updated_date
  is '更新时间';
alter table T_SYS_USER
  add constraint PK_T_SYS_USER primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

create table T_SYS_USER_ROLE_REL
(
  id          VARCHAR2(32) not null,
  role_id     VARCHAR2(32),
  user_id     VARCHAR2(32),
  create_by   VARCHAR2(32),
  create_time DATE
)
tablespace TS_TEST_DATA
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
comment on table T_SYS_USER_ROLE_REL
  is '系统-用户角色关系表 ';
alter table T_SYS_USER_ROLE_REL
  add constraint PK_SYS_USER_ROLE_REL primary key (ID)
  using index 
  tablespace TS_TEST_DATA
  pctfree 10
  initrans 2
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

注意:

  • 后台使用 shiro 进行权限控制时,根据用户信息进行登录验证,登录验证通过之后查询并设置角色、权限信息
  • 前端通过 用户的角色信息以及角色的菜单信息来获取用户下的菜单,通过用户的权限信息来设置菜单下的权限按钮是否可用

2. 代码实现

2.1 相关依赖

工程中主要使用 springboot, oracle 数据库,shiro, mybatis-plus,druid 数据库连接池,lombok,swagger2 相关依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.johnfnash.learn.springboot</groupId>
    <artifactId>shiro-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro-demo</name>
    <description>Demo project for shiro</description>

    <properties>
        <java.version>1.8</java.version>
        <spring.boot.version>2.0.1.RELEASE</spring.boot.version>
        <druid.version>1.1.10</druid.version>
        <oracle.version>11.2.0.3</oracle.version>
        <swagger.version>2.7.0</swagger.version>
        <shiro.version>1.4.0</shiro.version>
        <mybatis.plus.version>3.1.0</mybatis.plus.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        
        <!-- oracle -->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>${oracle.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        
        <!-- swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.0</version>
        </dependency>
        
        <!--mybatis plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>   

</project>

2.2 基本配置application.yml 和 application-dev.yml

application.yml

server:
  tomcat:
    uri-encoding: UTF-8

spring:
  application:
    name: phas-server
  profiles:
    active: dev
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: oracle.jdbc.OracleDriver
    filters: stat
    maxActive: 20
    initialSize: 1
    maxWait: 60000
    minIdle: 1
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: select 1 from dual
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    maxOpenPreparedStatements: 20
  mvc:
    # 默认spring mvc 接收Date类型参数转换格式
    dateFormat: yyyy-MM-dd HH:mm:ss
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: ALWAYS

application-dev.yml

server:
  port: 8081
  tomcat:
    accesslog:
      buffered: true
      directory: ../logs
      enabled: true
      file-date-format: .yyyy-MM-dd
      pattern: common
      prefix: access_log
      rename-on-rotate: false
      request-attributes-enabled: false
      rotate: true
      suffix: .log

# 数据源
spring:
  datasource:
    url: jdbc:oracle:thin:@ip:port:SID
    username: your_username
    password: your_password

# 登录
auth:
  adminSessionExpiresInSec: 7200

apiDoc:
  enable: true

#mybatisPlus
mybatis-plus:
  mapper-locations: classpath:mapper/**/*Mapper.xml
  typeAliasesPackage: com.johnfnash.learn.shiro.system.entity
  configuration:
    # org.apache.ibatis.logging.nologging.NoLoggingImpl
    # org.apache.ibatis.logging.slf4j.Slf4jImpl
    # org.apache.ibatis.logging.stdout.StdOutImpl
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

2.3 实体类

这里只列出 shiro 权限校验用到的相关实体类,菜单等实体类这里就不列出了。

系统用户表

package com.johnfnash.learn.shiro.system.entity;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import com.baomidou.mybatisplus.annotation.TableName;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;


/**
 * 系统用户表
 * 
 */
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@TableName("T_SYS_USER")
public class SysUser extends AbstractEntity implements Serializable {

    private static final long serialVersionUID = -7263855509985914106L;

    private String loginName;

    private String orgCode;

    private String passwd;

    private String phone;

    private Integer state;
    
    public interface State{
        int ACTIVE = 1;
        int INACTIVE = 2;
        int DELETED = 3; // 用户不能物理删除,只能逻辑删除。可能有业务表中存在该用户的id
    }

    private String userName;

    private Integer userType;
    
    public interface UserType{
        int SUPER_ADMIN = 1;
        int SYSTEM_USER = 2;
    }
    
    // 用户所有角色值,用于shiro做角色权限的判断
    private Set<String> roles = Collections.synchronizedSet(new HashSet<String>(0));

    // 用户所有权限值,用于shiro做资源权限的判断
    private Set<String> stringPermissions = Collections.synchronizedSet(new HashSet<String>(0));

}

角色表

package com.johnfnash.learn.shiro.system.entity;

import com.baomidou.mybatisplus.annotation.TableName;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;


/**
 * 角色表
 * 
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("T_SYS_ROLE")
public class SysRole extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    private String descs;

    private String roleName;

    private Integer state;
    
    public interface State{
        int ACTIVE = 1;
        int INACTIVE = 2;
    }

}

权限表

package com.johnfnash.learn.shiro.system.entity;

import com.baomidou.mybatisplus.annotation.TableName;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;


/**
 * 权限表
 * 
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("T_SYS_PERMISSION")
public class SysPermission extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    private String descs;

    private String perCode;

    private String perName;

    private Integer state;
    
    public interface State{
        int ACTIVE = 1;
        int INACTIVE = 2;
    }

}

其中 AbstractEntity 类代码如下:

package com.johnfnash.learn.shiro.system.entity;

import java.io.Serializable;
import java.util.Date;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;

import lombok.Data;

/**
 * 数据库对象抽象类
 */
@Data
public abstract class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 5952973214101216907L;

    @TableId(type = IdType.UUID)
    private String id;
    
    private String createdBy;

    private Date createdDate;

    private String updatedBy;

    private Date updatedDate;
    
}

2.4 Mapper

2.4.1 SysUserMapper
package com.johnfnash.learn.shiro.system.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.johnfnash.learn.shiro.system.entity.SysUser;
import com.johnfnash.learn.shiro.system.vo.FindUserListReq;

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

    @Select("select * from T_SYS_USER where LOGIN_NAME = #{userName}")
    public SysUser findUserByUserName(@Param("userName") String userName);
    
    // 非shiro权限校验相关
    public List<SysUser> findAll(Page<SysUser> page, @Param("req") FindUserListReq req);
    
}

相应 xml - SysUserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.johnfnash.learn.shiro.system.mapper.SysUserMapper">
    
   <select id="findAll" resultType="com.johnfnash.learn.shiro.system.entity.SysUser">
        select * 
        from T_SYS_USER
        where STATE != 3 
        <if test="req.userName != null and req.userName != ''">
            and USER_NAME like #{req.userName}
        </if>
   </select>
   
</mapper>
2.4.2 SysRoleMapper

SysRoleMapper

package com.johnfnash.learn.shiro.system.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.johnfnash.learn.shiro.system.entity.SysRole;

@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {

    public List<String> findRolesByUserId(@Param("userId") String userId);
    
}

相应 xml - SysRoleMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.johnfnash.learn.shiro.system.mapper.SysRoleMapper">
    
   <select id="findRolesByUserId" resultType="String">
        select r.ID from T_SYS_ROLE r
        join T_SYS_USER_ROLE_REL rel on rel.ROLE_ID = r.ID
        where r.STATE = 1 and rel.USER_ID = #{userId}   
   </select>
   
</mapper>
2.4.3 SysPermissionMapper

SysPermissionMapper

package com.johnfnash.learn.shiro.system.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.johnfnash.learn.shiro.system.entity.SysPermission;

@Mapper
public interface SysPermissionMapper extends BaseMapper<SysPermission> {

    public List<String> findPermissionCodesByUser(@Param("userId") String userId);
    
}

相应 xml - SysPermissionMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.johnfnash.learn.shiro.system.mapper.SysPermissionMapper">
    
   <select id="findPermissionCodesByUser" resultType="String">
        select P.ID from T_SYS_PERMISSION p
        join T_SYS_ROLE_PER_REL rel on rel.PER_ID = p.ID
        join T_SYS_USER_ROLE_REL ur on ur.ROLE_ID = rel.ROLE_ID
        where p.STATE = 1 and ur.USER_ID = #{userId}        
   </select>
   
</mapper>

2.5 Service

Service 就是简单的调用 Mapper, 这里就不介绍了。

2.6 自定义 AuthorizingRealm 来进行用户认证、角色权限校验

package com.johnfnash.learn.shiro.auth;

import java.util.List;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
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.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import com.johnfnash.learn.shiro.system.entity.SysUser;
import com.johnfnash.learn.shiro.system.service.SysPermissionService;
import com.johnfnash.learn.shiro.system.service.SysRoleService;
import com.johnfnash.learn.shiro.system.service.SysUserService;

@Component
public class AuthRealm extends AuthorizingRealm {

    /**
     * 此处必须加 @Lazy 注解使 UserService 被 BeanPostProcessor 拦截,否则事务会失效
     */
    @Lazy
    @Autowired
    private SysUserService userService;
    
    @Lazy
    @Autowired
    private SysPermissionService permissionService;
    
    @Lazy
    @Autowired
    private SysRoleService roleService;
    
    // 角色权限和对应权限添加
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取session中的用户
        SysUser user = (SysUser) principals.fromRealm(this.getClass().getName()).iterator().next();
        // 取出用户的权限列表和角色列表
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermissions(user.getStringPermissions());
        info.addRoles(user.getRoles());
        return info;
    }

    // 用户认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //加这一步的目的是在Post请求的时候会先进认证,然后在到请求
        if (token.getPrincipal() == null) {
            return null;
        }
        
        UsernamePasswordToken uToken = (UsernamePasswordToken) token;
        String username = uToken.getUsername();
        
        // 获取用户信息
        SysUser user = userService.findUserByUserName(username);
        // 验证用户是否存在以及账号是否可用
        if(user == null) {
            throw new UnknownAccountException();
        }
        if (user != null && user.getState().intValue() != SysUser.State.ACTIVE) {
            throw new DisabledAccountException();
        }
        
        // 查询用户的权限以及角色
        List<String> permissions = permissionService.findPermissionCodesByUser(user.getId());
        user.getStringPermissions().addAll(permissions);
        List<String> roles = roleService.findRolesByUserId(user.getId());
        user.getRoles().addAll(roles);
        
        return new SimpleAuthenticationInfo(user, user.getPasswd(), this.getClass().getName());
    }
    
}

另外,添加同一异常处理,用于处理认证、授权相关的异常。

package com.johnfnash.learn.shiro.common.interceptor;

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.authz.AuthorizationException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.johnfnash.learn.shiro.common.dto.JsonResult;
import com.johnfnash.learn.shiro.common.enums.StatusCodeEnum;

import lombok.extern.slf4j.Slf4j;

/**
 * 统一异常处理
 *
 */
@Slf4j
@ControllerAdvice
@ResponseBody
public class UnifiedExceptionHandler {

    @ExceptionHandler(value = AuthorizationException.class)
    public JsonResult<?> handleAuthorizationException(HttpServletRequest request,
            HttpServletResponse response, AuthorizationException ex) {
        log.error(ex.getMessage());
        
        JsonResult<?> jr = JsonResult.of(StatusCodeEnum.AUTH_ERR.getCode(), null);
        if (ex instanceof org.apache.shiro.authz.UnauthenticatedException) {
            // 退出登录
            SecurityUtils.getSubject().logout();
            jr.setMsg("由于您长时间未操作,系统已自动退出,请重新登录");
        } else if (ex instanceof org.apache.shiro.authz.UnauthorizedException) {
            // 无权限操作
            jr.setCode(StatusCodeEnum.NO_PERMISSION.getCode());
            jr.setMsg("权限不足,请联系管理员");
        } else {
            // 退出登录
            SecurityUtils.getSubject().logout();
            jr.setMsg("授权校验失败");
        }

        return jr;
    }

    @ExceptionHandler(value = AuthenticationException.class)
    public JsonResult<?> handleAuthenticationException(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException ex) {
        log.error(ex.getMessage());
        
        SecurityUtils.getSubject().logout();
        JsonResult<?> jr = JsonResult.of(StatusCodeEnum.AUTH_ERR.getCode(), null);

        if (ex instanceof org.apache.shiro.authc.ConcurrentAccessException) {
            jr.setMsg("重复登陆");
        } else if (ex instanceof org.apache.shiro.authc.CredentialsException) {
            if (ex instanceof org.apache.shiro.authc.IncorrectCredentialsException) {
                jr.setMsg("密码错误");
            } else if (ex instanceof org.apache.shiro.authc.ExpiredCredentialsException) {
                jr.setMsg("过期证书");
            } else {
                jr.setMsg("证书错误");
            }
        } else if (ex instanceof org.apache.shiro.authc.DisabledAccountException) {
            jr.setMsg("账号不可用");
        } else if (ex instanceof org.apache.shiro.authc.ExcessiveAttemptsException) {
            jr.setMsg("失败次数过多");
        } else if (ex instanceof org.apache.shiro.authc.LockedAccountException) {
            jr.setMsg("账号被锁定");
        } else if (ex instanceof org.apache.shiro.authc.UnknownAccountException) {
            jr.setMsg(ex.getMessage() != null ? ex.getMessage() : "未知账号");
        } else {
            jr.setMsg("认证失败");
        }

        return jr;
    }
    
    @ExceptionHandler(value = ServletRequestBindingException.class)
    public JsonResult<?> handleServletRequestBindingException(HttpServletRequest request,
            HttpServletResponse response, ServletRequestBindingException ex) {
        log.error(ex.getMessage());
        
        SecurityUtils.getSubject().logout();
        JsonResult<?> jr = JsonResult.of(StatusCodeEnum.ILLEGAL_PARAM.getCode(), null);

        if(ex instanceof MissingServletRequestParameterException) {
            jr.setMsg(ex.getMessage() != null ? ex.getMessage() : "缺少请求参数");
        } else if(ex instanceof MissingPathVariableException) {
            jr.setMsg(ex.getMessage() != null ? ex.getMessage() : "缺少路径参数");
        } else {
            jr.setMsg("接口调用请求绑定异常");
        }
        
        return jr;
    }
    
}
2.7 Shiro 配置
package com.johnfnash.learn.shiro.common.config;

import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import com.johnfnash.learn.shiro.auth.AuthRealm;

@Configuration
public class ShiroConfig {

    @Bean
    public ServletContainerSessionManager shiroServletContainerSessionManager() {
        return new ServletContainerSessionManager();
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        creator.setProxyTargetClass(true);
        return creator;
    }

    // 加入注解的使用,不加入这个注解不生效
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            @Qualifier("securityManager") SecurityManager manager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(manager);
        return advisor;
    }

    // 配置核心安全事务管理器
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(ServletContainerSessionManager shiroServletContainerSessionManager,
            @Qualifier("authRealm") AuthRealm authRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 用于缓存用户校验相关信息
        manager.setCacheManager(new MemoryConstrainedCacheManager());
        manager.setSessionManager(shiroServletContainerSessionManager);
        // 设置自定义 realm
        manager.setRealm(authRealm);
        return manager;
    }
    
    //Filter工厂
    @Bean
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager shiroSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(shiroSecurityManager);
        return shiroFilterFactoryBean;
    }

}

2.8 shiro 校验使用

shiro 可以只用注解来做鉴权授权,也可以使用url配置来做鉴权授权,也可以前面两者一起使用来进行鉴权授权。本文介绍使用注解来做鉴权授权。要对比这三种方式的使用,可以参考:Shiro用starter方式优雅整合到SpringBoot中

2.8.1 登录/登出
package com.johnfnash.learn.shiro.system.action;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.johnfnash.learn.shiro.common.action.BaseAction;
import com.johnfnash.learn.shiro.common.dto.JsonResult;
import com.johnfnash.learn.shiro.system.entity.SysUser;
import com.johnfnash.learn.shiro.system.vo.LoginReq;
import com.johnfnash.learn.shiro.system.vo.LoginRsp;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;

/**
 * 登录登出
 */
@Api(tags = "登录登出")
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthAction extends BaseAction {
    @Value("${auth.adminSessionExpiresInSec:1800}")
    private long adminSessionExpiresInSec;
    
    @ApiOperation(value = "登录", notes = "登录")
    @PostMapping(value = "/login")
    public JsonResult<LoginRsp> login(@Valid @RequestBody LoginReq loginReq, HttpServletRequest request) {
        Subject subject = SecurityUtils.getSubject();
        if (!subject.isAuthenticated()) {
            String username = loginReq.getLoginName();
            String password = loginReq.getPasswd();

            UsernamePasswordToken token = new UsernamePasswordToken();
            token.setUsername(username);
            token.setPassword(password.toCharArray());

            subject.login(token);

            Session session = getSession(false);
            session.setTimeout(adminSessionExpiresInSec * 1000L);
        }

        SysUser principal = (SysUser) getPrincipal();

        LoginRsp loginRsp = new LoginRsp();
        loginRsp.setSessionId((String) getSession(false).getId());
        loginRsp.getRoleCodes().addAll(principal.getRoles());
        loginRsp.getPermissionCodes().addAll(principal.getStringPermissions());
        loginRsp.setName(principal.getUserName());
        
        return JsonResult.of(loginRsp);
    }
    
    @ApiOperation(value = "注销")
    @GetMapping(value = "/logout")
    @RequiresAuthentication
    public JsonResult<Void> logout() {
        getSubject().logout();
        return JsonResult.of();
    }

}

注:登录成功之后,返回用户的sessionId,权限列表,角色列表给前端。

2.8.4 业务action中shiro注解使用
package com.johnfnash.learn.shiro.system.action;

import javax.validation.Valid;

import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.johnfnash.learn.shiro.common.action.BaseAction;
import com.johnfnash.learn.shiro.common.dto.JsonResult;
import com.johnfnash.learn.shiro.common.dto.PageInfo;
import com.johnfnash.learn.shiro.system.service.SysUserService;
import com.johnfnash.learn.shiro.system.vo.FindUserListReq;
import com.johnfnash.learn.shiro.system.vo.SysUserVo;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Api(tags = "系统管理-用户管理")
@RestController
@RequestMapping("/sys/user")
@RequiresAuthentication
public class UserAction extends BaseAction {

    @Autowired
    private SysUserService userService;
    
    @ApiOperation(value = "分页查询用户列表", notes = "分页查询用户列表")
    @PostMapping(value = "/list", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @RequiresPermissions(value = "per_01_01_03_01")
    public JsonResult<PageInfo<SysUserVo>> list(@RequestBody @Valid FindUserListReq req) {
        return JsonResult.of(userService.list(req));
    }
    
}

这里使用了 @RequiresAuthentication 用于限制必选登录之后才能访问action下的接口,@RequiresPermissions用于限制必须有指定的权限才能访问相关接口。

shiro提供了以下几种注解来做控制。可以根据需要进行使用。

注解功能
@RequiresGuest只有游客可以访问
@RequiresAuthentication需要登录才能访问
@RequiresUser已登录的用户或“记住我”的用户能访问
@RequiresRoles已登录的用户需具有指定的角色才能访问
@RequiresPermissions已登录的用户需具有指定的权限才能访问

相关代码已经上传至 github,需要的可以访问:https://github.com/JohnFNash/springboot-examples/tree/master/shiro-demo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值