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