Spring Boot 极简集成 Shiro

文章出处(转载自乐字节)

1. 前言

Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

Shiro有三大核心组件:

Subject:即当前用户,在权限管理的应用程序里往往需要知道谁能够操作什么,谁拥有操作该程序的权利,shiro中则需要通过Subject来提供基础的当前用户信息,Subject 不仅仅代表某个用户,与当前应用交互的任何东西都是Subject,如网络爬虫等。所有的Subject都要绑定到SecurityManager上,与Subject的交互实际上是被转换为与SecurityManager的交互。

SecurityManager:即所有Subject的管理者,这是Shiro框架的核心组件,可以把他看做是一个Shiro框架的全局管理组件,用于调度各种Shiro框架的服务。作用类似于SpringMVC中的DispatcherServlet,用于拦截所有请求并进行处理。

Realm:Realm是用户的信息认证器和用户的权限人证器,我们需要自己来实现Realm来自定义的管理我们自己系统内部的权限规则。SecurityManager要验证用户,需要从Realm中获取用户。可以把Realm看做是数据源。

2. 数据库设计

2.1 User(用户)

SETNAMESutf8mb4;

SETFOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for user

-- ----------------------------

DROPTABLEIFEXISTS`user`;

CREATETABLE`user`(

`id`bigint(20) NOTNULLAUTO_INCREMENT,

`password`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULL,

`username`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULL,

`account`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULL,

PRIMARY KEY(`id`) USINGBTREE

) ENGINE= MyISAM AUTO_INCREMENT = 4CHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of user

-- ----------------------------

INSERTINTO`user`VALUES(1, 'root', '超级用户', 'root');

INSERTINTO`user`VALUES(2, 'user', '普通用户', 'user');

INSERTINTO`user`VALUES(3, 'vip', 'VIP用户', 'vip');

SETFOREIGN_KEY_CHECKS = 1;

2.2 Role(角色)

SETNAMESutf8mb4;

SETFOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for role

-- ----------------------------

DROPTABLEIFEXISTS`role`;

CREATETABLE`role`(

`id`int(11) NOTNULLAUTO_INCREMENT,

`role`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULL,

`desc`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULL,

PRIMARY KEY(`id`) USINGBTREE

) ENGINE= MyISAM AUTO_INCREMENT = 4CHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of role

-- ----------------------------

INSERTINTO`role`VALUES(1, 'admin', '超级管理员');

INSERTINTO`role`VALUES(2, 'user', '普通用户');

INSERTINTO`role`VALUES(3, 'vip_user', 'VIP用户');

SETFOREIGN_KEY_CHECKS = 1;

2.3 Permission(权限)

SETNAMESutf8mb4;

SETFOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for permission

-- ----------------------------

DROPTABLEIFEXISTS`permission`;

CREATETABLE`permission`(

`id`int(11) NOTNULLAUTO_INCREMENT,

`permission`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULLCOMMENT'权限名称',

`desc`varchar(255) CHARACTERSETutf8 COLLATEutf8_general_ci NULLDEFAULTNULLCOMMENT'权限描述',

PRIMARY KEY(`id`) USINGBTREE

) ENGINE= MyISAM AUTO_INCREMENT = 5CHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of permission

-- ----------------------------

INSERTINTO`permission`VALUES(1, 'add', '增加');

INSERTINTO`permission`VALUES(2, 'update', '更新');

INSERTINTO`permission`VALUES(3, 'select', '查看');

INSERTINTO`permission`VALUES(4, 'delete', '删除');

SETFOREIGN_KEY_CHECKS = 1;

2.4 User_Role(用户-角色)

SETNAMESutf8mb4;

SETFOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for user_role

-- ----------------------------

DROPTABLEIFEXISTS`user_role`;

CREATETABLE`user_role`(

`id`int(11) NOTNULLAUTO_INCREMENT,

`user_id`int(11) NULLDEFAULTNULL,

`role_id`int(11) NULLDEFAULTNULL,

PRIMARY KEY(`id`) USINGBTREE

) ENGINE= MyISAM AUTO_INCREMENT = 4CHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Fixed;

-- ----------------------------

-- Records of user_role

-- ----------------------------

INSERTINTO`user_role`VALUES(1, 1, 1);

INSERTINTO`user_role`VALUES(2, 2, 2);

INSERTINTO`user_role`VALUES(3, 3, 3);

SETFOREIGN_KEY_CHECKS = 1;

2.5 Role_Permission(角色-权限)

SETNAMESutf8mb4;

SETFOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for role_permission

-- ----------------------------

DROPTABLEIFEXISTS`role_permission`;

CREATETABLE`role_permission`(

`id`int(11) NOTNULLAUTO_INCREMENT,

`role_id`int(11) NULLDEFAULTNULL,

`permission_id`int(255) NULLDEFAULTNULL,

PRIMARY KEY(`id`) USINGBTREE

) ENGINE= MyISAM AUTO_INCREMENT = 9CHARACTERSET= utf8 COLLATE= utf8_general_ci ROW_FORMAT = Fixed;

-- ----------------------------

-- Records of role_permission

-- ----------------------------

INSERTINTO`role_permission`VALUES(1, 1, 1);

INSERTINTO`role_permission`VALUES(2, 1, 2);

INSERTINTO`role_permission`VALUES(3, 1, 3);

INSERTINTO`role_permission`VALUES(4, 1, 4);

INSERTINTO`role_permission`VALUES(5, 2, 3);

INSERTINTO`role_permission`VALUES(6, 3, 3);

INSERTINTO`role_permission`VALUES(7, 3, 2);

INSERTINTO`role_permission`VALUES(8, 2, 1);

SETFOREIGN_KEY_CHECKS = 1;

3. 项目结构

4. 前期准备

4.1 导入Pom

org.springframework.boot

spring-boot-starter-web

mysql

mysql-connector-java

org.mybatis.spring.boot

mybatis-spring-boot-starter

1.3.2

org.apache.shiro

shiro-spring

1.4.0

4.2 application.yml

server:

port: 8903

spring:

application:

name: lab-user

datasource:

driver-class-name: com.mysql.jdbc.Driver

url: jdbc:mysql://127.0.0.1:3306/laboratory?charset=utf8

username: root

password: root

mybatis:

type-aliases-package: cn.ntshare.laboratory.entity

mapper-locations: classpath:mapper/*.xml

configuration:

map-underscore-to-camel-case: true

4.3 实体类

4.3.1 User.java

@Data

@ToString

publicclassUserimplementsSerializable{

privatestaticfinallongserialVersionUID = -6056125703075132981L;

privateInteger id;

privateString account;

privateString password;

privateString username;

}

4.3.2 Role.java

@Data

@ToString

publicclassRoleimplementsSerializable{

privatestaticfinallongserialVersionUID = -1767327914553823741L;

privateInteger id;

privateString role;

privateString desc;

}

4.4 Dao层

4.4.1 PermissionMapper.java

@Mapper

@Repository

publicinterfacePermissionMapper {

List findByRoleId(@Param("roleIds") List roleIds);

}

4.4.2 PermissionMapper.xml

id, permission, desc

selectpermission

frompermission, role_permission rp

whererp.permission_id = permission.id and rp.role_id in

#{id}

4.4.3 RoleMapper.java

@Mapper

@Repository

publicinterfaceRoleMapper {

List findRoleByUserId(@Param("userId") Integer userId);

}

4.4.4 RoleMapper.xml

id, user_id, role_id

selectrole.id, role

fromrole, user, user_role ur

whererole.id = ur.role_id and ur.user_id = user.id and user.id = #{userId}

4.4.5 UserMapper.java

@Mapper

@Repository

public interface UserMapper {

UserfindByAccount(@Param("account") String account);

}

4.4.6 UserMapper.xml

id, account, password, username

select

fromuser

whereaccount = #{account}

4.5 Service层

4.5.1 PermissionServiceImpl.java

@Service

publicclassPermissionServiceImplimplementsPermissionService{

@Autowired

privatePermissionMapper permissionMapper;

@Override

publicList findByRoleId(List<Integer> roleIds){

returnpermissionMapper.findByRoleId(roleIds);

}

}

4.5.2 RoleServiceImpl.java

@Service

publicclassRoleServiceImplimplementsRoleService{

@Autowired

privateRoleMapper roleMapper;

@Override

publicList findRoleByUserId(Integer id){

returnroleMapper.findRoleByUserId(id);

}

}

4.5.3 UserServiceImpl.java

@Service

publicclassUserServiceImplimplementsUserService{

@Autowired

privateUserMapper userMapper;

@Override

publicUser findByAccount(String account){

returnuserMapper.findByAccount(account);

}

}

4.6. 系统返回状态枚举与包装函数

4.6.1 ServerResponseEnum.java

@AllArgsConstructor

@Getter

public enum ServerResponseEnum {

SUCCESS(0, "成功"),

ERROR(10, "失败"),

ACCOUNT_NOT_EXIST(11, "账号不存在"),

DUPLICATE_ACCOUNT(12, "账号重复"),

ACCOUNT_IS_DISABLED(13, "账号被禁用"),

INCORRECT_CREDENTIALS(14, "账号或密码错误"),

NOT_LOGIN_IN(15, "账号未登录"),

UNAUTHORIZED(16, "没有权限")

;

Integercode;

Stringmessage;

}

4.6.2 ServerResponseVO.java

@Getter

@Setter

@NoArgsConstructor

publicclassServerResponseVO implementsSerializable{

privatestaticfinallongserialVersionUID = -1005863670741860901L;

// 响应码

privateInteger code;

// 描述信息

privateString message;

// 响应内容

privateT data;

privateServerResponseVO(ServerResponseEnum responseCode){

this.code = responseCode.getCode();

this.message = responseCode.getMessage();

}

privateServerResponseVO(ServerResponseEnum responseCode, T data){

this.code = responseCode.getCode();

this.message = responseCode.getMessage();

this.data = data;

}

privateServerResponseVO(Integer code, String message){

this.code = code;

this.message = message;

}

/**

* 返回成功信息

* @paramdata 信息内容

* @param

* @return

*/

publicstatic ServerResponseVO success(T data){

returnnewServerResponseVO<>(ServerResponseEnum.SUCCESS, data);

}

/**

* 返回成功信息

* @return

*/

publicstaticServerResponseVO success(){

returnnewServerResponseVO(ServerResponseEnum.SUCCESS);

}

/**

* 返回错误信息

* @paramresponseCode 响应码

* @return

*/

publicstaticServerResponseVO error(ServerResponseEnum responseCode){

returnnewServerResponseVO(responseCode);

}

}

4.7 统一异常处理

当用户身份认证失败时,会抛出UnauthorizedException,我们可以通过统一异常处理来处理该异常

@RestControllerAdvice

public class UserExceptionHandler {

@ExceptionHandler(UnauthorizedException.class)

@ResponseStatus(HttpStatus.UNAUTHORIZED)

public ServerResponseVO UnAuthorizedExceptionHandler(UnauthorizedException e) {

returnServerResponseVO.error(ServerResponseEnum.UNAUTHORIZED);

}

}

5. 集成Shiro

5.1 UserRealm.java

/**

* 负责认证用户身份和对用户进行授权

*/

publicclassUserRealmextendsAuthorizingRealm{

@Autowired

privateUserService userService;

@Autowired

privateRoleService roleService;

@Autowired

privatePermissionService permissionService;

// 用户授权

protectedAuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection){

User user = (User) principalCollection.getPrimaryPrincipal();

SimpleAuthorizationInfo authorizationInfo = newSimpleAuthorizationInfo();

List roleList = roleService.findRoleByUserId(user.getId());

Set roleSet = newHashSet<>();

List roleIds = newArrayList<>();

for(Role role : roleList) {

roleSet.add(role.getRole());

roleIds.add(role.getId());

}

// 放入角色信息

authorizationInfo.setRoles(roleSet);

// 放入权限信息

List permissionList = permissionService.findByRoleId(roleIds);

authorizationInfo.setStringPermissions(newHashSet<>(permissionList));

returnauthorizationInfo;

}

// 用户认证

protectedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken)throwsAuthenticationException {

UsernamePasswordToken token = (UsernamePasswordToken) authToken;

User user = userService.findByAccount(token.getUsername());

if(user == null) {

returnnull;

}

returnnewSimpleAuthenticationInfo(user, user.getPassword(), getName());

}

}

5.2 ShiroConfig.java

@Configuration

publicclassShiroConfig{

@Bean

publicUserRealm userRealm(){

returnnewUserRealm();

}

@Bean

publicDefaultWebSecurityManager securityManager(){

DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager();

securityManager.setRealm(userRealm());

returnsecurityManager;

}

/**

* 路径过滤规则

* @return

*/

@Bean

publicShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){

ShiroFilterFactoryBean shiroFilterFactoryBean = newShiroFilterFactoryBean();

shiroFilterFactoryBean.setSecurityManager(securityManager);

shiroFilterFactoryBean.setLoginUrl("/login");

shiroFilterFactoryBean.setSuccessUrl("/");

Map map = newLinkedHashMap<>();

// 有先后顺序

map.put("/login", "anon"); // 允许匿名访问

map.put("/**", "authc"); // 进行身份认证后才能访问

shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

returnshiroFilterFactoryBean;

}

/**

* 开启Shiro注解模式,可以在Controller中的方法上添加注解

* @paramsecurityManager

* @return

*/

@Bean

publicAuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager")DefaultSecurityManager securityManager) {

AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = newAuthorizationAttributeSourceAdvisor();

authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);

returnauthorizationAttributeSourceAdvisor;

}

5.3 LoginController.java

@RestController

@RequestMapping("")

publicclassLoginController {

@PostMapping("/login")

publicServerResponseVO login(@RequestParam(value = "account") Stringaccount,

@RequestParam(value = "password") Stringpassword) {

Subject userSubject = SecurityUtils.getSubject();

UsernamePasswordToken token = newUsernamePasswordToken(account, password);

try{

// 登录验证

userSubject.login(token);

returnServerResponseVO.success();

} catch(UnknownAccountException e) {

returnServerResponseVO.error(ServerResponseEnum.ACCOUNT_NOT_EXIST);

} catch(DisabledAccountException e) {

returnServerResponseVO.error(ServerResponseEnum.ACCOUNT_IS_DISABLED);

} catch(IncorrectCredentialsException e) {

returnServerResponseVO.error(ServerResponseEnum.INCORRECT_CREDENTIALS);

} catch(Throwable e) {

e.printStackTrace();

returnServerResponseVO.error(ServerResponseEnum.ERROR);

}

}

@GetMapping("/login")

publicServerResponseVO login() {

returnServerResponseVO.error(ServerResponseEnum.NOT_LOGIN_IN);

}

@GetMapping("/auth")

publicStringauth() {

return"已成功登录";

}

@GetMapping("/role")

@RequiresRoles("vip")

publicStringrole() {

return"测试Vip角色";

}

@GetMapping("/permission")

@RequiresPermissions(value = {"add", "update"}, logical = Logical.AND)

publicStringpermission() {

return"测试Add和Update权限";

}

}

6. 测试

6.1 用root用户登录

6.1.1 登录

6.1.2 验证是否登录

6.1.3 测试角色权限

6.1.4 测试用户操作权限

7. 总结

本文演示了 Spring Boot 极简集成 Shiro 框架,实现了基础的身份认证和授权功能,如有不足,请多指教。

后续可扩展的功能点有:

1. 集成 Redis 实现 Shiro 的分布式会话

2. 集成 JWT 实现单点登录功能

如果看到这里,说明你喜欢这篇文章,761935205这是我的java无广告交流裙,进裙暗号Z17,有任何问题可以随时来咨询我;感兴趣的朋友也可以进来学习下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容