Spring Boot2(十二):手摸手教你搭建Shiro安全框架

1、前言

SpringBoot+Shiro+Mybatis完成的。java

以前看了一位小伙伴的Shiro教程,跟着作了,遇到蛮多坑的(´இ皿இ`)mysql

修改整理了一下,成功跑起来了。能够经过postman进行测试git

很少比比∠( ᐛ 」∠)_,直接上源码:github.com/niaobulashi…github

2、Shiro是啥

Apache Shiro是一个功能强大、灵活的、开源的安全框架。能够干净利落地处理身份验证、受权、企业会话管理和加密。web

2、Shiro能够干啥

  • 验证用户身份
  • 用户访问权限控制,好比:一、判断用户是否分配了必定的安全角色。二、判断用户是否被授予完成某个操做的权限
  • 在非 Web 或 EJB 容器的环境下能够任意使用 Session API
  • 能够响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
  • 支持单点登陆(SSO)功能
  • 支持提供“Remember Me”服务,获取用户关联信息而无需登陆

Shiro框架图以下:算法

img

  • **Authentication(认证):**用户身份识别,一般被称为用户“登陆”
  • **Authorization(受权):**访问控制。好比某个用户是否具备某个操做的使用权限。
  • **Session Management(会话管理):**特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
  • **Cryptography(加密):**在对数据源使用加密算法加密的同时,保证易于使用。

在概念层,Shiro架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展现了这些组件如何相互做用,咱们将在下面依次对其进行描述。spring

img

  • Subject:当前用户,Subject 能够是一我的,但也能够是第三方服务、守护进程账户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • SecurityManager:管理全部Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,咱们本身实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源链接的细节,获得Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或受权(authorization)。

3、代码实现

一、添加Maven依赖

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
复制代码

二、配置文件

application.ymlsql

# 服务器端口
server:
 port: 8081

# 配置Spring相关信息
spring:
 datasource:
 driver-class-name: com.mysql.jdbc.Driver
 url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
 username: root
 password: root

# 配置Mybatis
mybatis:
 type-aliases-package: com.niaobulashi.model
 mapper-locations: classpath:mapper/*.xml
 configuration:
    # 开启驼峰命名转换
 map-underscore-to-camel-case: true

# 打印SQL日志
logging:
 level:
    com.niaobulashi.mapper: DEBUG
复制代码

启动方法添加mapper扫描,我通常都是在启动方法上面声明,不然须要在每个mapper上单独声明扫描数据库

@SpringBootApplication
@MapperScan("com.niaobulashi.mapper")
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }
}
复制代码

三、简单的表设计

无非就是5张表:用户表、角色表、权限表、用户角色表、角色权限表。apache

看下面这张图,能够说至关明了了。

具体我就不贴出来了,太占篇幅。。直接贴连接:github.com/niaobulashi…

四、实体类

User.java

@Data
public class User implements Serializable {

    private static final long serialVersionUID = -6056125703075132981L;

    private Integer id;

    private String account;

    private String password;

    private String username;
}
复制代码

Role.java

@Data
public class Role implements Serializable {

    private static final long serialVersionUID = -1767327914553823741L;

    private Integer id;

    private String role;

    private String desc;
}
复制代码

五、mapper层

这里归纳一下:简单的用户登陆权限的Shiro控制涉及到的数据库操做主要有仨

  • 用户登陆名查询用户信息
  • 根据用户查询角色信息
  • 根据角色查询权限信息

UserMapper.java/UserMapper.xml

public interface UserMapper {
    /** * 根据帐户查询用户信息 * @param account * @return */
    User findByAccount(@Param("account") String account);
}
复制代码
<!--用户表结果集-->
<sql id="base_column_list">
	id, account, password, username
</sql>

<!--根据帐户查询用户信息-->
<select id="findByAccount" parameterType="Map" resultType="com.niaobulashi.model.User">
	select
	<include refid="base_column_list"/>
	from user
	where account = #{account}
</select>
复制代码

RoleMapper.java/RoleMapper.xml

public interface RoleMapper {
    /**
     * 根据userId查询角色信息
     * @param userId
     * @return
     */
    List<Role> findRoleByUserId(@Param("userId") Integer userId);
}
复制代码
<!--角色表字段结果集-->
<sql id="base_cloum_list">
	id, role, desc
</sql>

<!--根据userId查询角色信息-->
<select id="findRoleByUserId" parameterType="Integer" resultType="com.niaobulashi.model.Role">
	select r.id, r.role
	from role r
	left join user_role ur on ur.role_id = r.id
	left join user u on u.id = ur.user_id
	where 1=1
	and u.user_id = #{userId}
</select>
复制代码

PermissionMapper.java/PermissionMapper.xml

public interface PermissionMapper {
    /** * 根据角色id查询权限 * @param roleIds * @return */
    List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds);
}
复制代码
<!--权限查询结果集-->
<sql id="base_column_list">
	id, permission, desc
</sql>

<!--根据角色id查询权限-->
<select id="findByRoleId" parameterType="List" resultType="String">
	select permission
	from permission, role_permission rp
	where rp.permission_id = permission.id and rp.role_id in
	<foreach collection="roleIds" item="id" open="(" close=")" separator=",">
		#{id}
	</foreach>
</select>
复制代码

六、Service层

没有其余逻辑,只有继承。

注意:

不过须要注意的一点是,我在Service层中,使用的注解@Service:启动时会自动注册到Spring容器中。

不然启动时,拦截器配置初始化时,会找不到Service。。。这点有点坑。

UserService.java/UserServiceImpl.java

public interface UserService {
    /** * 根据帐户查询用户信息 * @param account * @return */
    User findByAccount(String account);
}
复制代码
@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    /** * 根据帐户查询用户信息 * @param account * @return */
    @Override
    public User findByAccount(String account) {
        return userMapper.findByAccount(account);
    }
}
复制代码

RoleService.java/RoleServiceImpl.java

public interface RoleService {

    /** * 根据userId查询角色信息 * @param id * @return */
    List<Role> findRoleByUserId(Integer id);
}
复制代码
@Service("roleService")
public class RoleServiceImpl implements RoleService {

    @Resource
    private RoleMapper roleMapper;

    /** * 根据userId查询角色信息 * @param id * @return */
    @Override
    public List<Role> findRoleByUserId(Integer id) {
        return roleMapper.findRoleByUserId(id);
    }
}
复制代码

PermissionService.java/PermissionServiceImpl.java

public interface PermissionService {

    /** * 根据角色id查询权限 * @param roleIds * @return */
    List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds);
}
复制代码
@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {

    @Resource
    private PermissionMapper permissionMapper;

    /** * 根据角色id查询权限 * @param roleIds * @return */
    @Override
    public List<String> findByRoleId(List<Integer> roleIds) {
        return permissionMapper.findByRoleId(roleIds);
    }
}
复制代码

七、系通通一返回状态枚举和包装方法

状态字段枚举

StatusEnmus.java

public enum StatusEnums {

    SUCCESS(200, "操做成功"),
    SYSTEM_ERROR(500, "系统错误"),
    ACCOUNT_UNKNOWN(500, "帐户不存在"),
    ACCOUNT_IS_DISABLED(13, "帐号被禁用"),
    INCORRECT_CREDENTIALS(500,"用户名或密码错误"),
    PARAM_ERROR(400, "参数错误"),
    PARAM_REPEAT(400, "参数已存在"),
    PERMISSION_ERROR(403, "没有操做权限"),
    NOT_LOGIN_IN(15, "帐号未登陆"),
    OTHER(-100, "其余错误");

    @Getter
    @Setter
    private int code;
    @Getter
    @Setter
    private String message;

    StatusEnums(int code, String message) {
        this.code = code;
        this.message = message;
    }
}
复制代码

响应包装方法

ResponseCode.java

@Data
@AllArgsConstructor
public class ResponseCode<T> implements Serializable {

    private Integer code;
    private String message;
    private Object data;

    private ResponseCode(StatusEnums responseCode) {
        this.code = responseCode.getCode();
        this.message = responseCode.getMessage();
    }

    private ResponseCode(StatusEnums responseCode, T data) {
        this.code = responseCode.getCode();
        this.message = responseCode.getMessage();
        this.data = data;
    }

    private ResponseCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    /** * 返回成功信息 * @param data 信息内容 * @param <T> * @return */
    public static<T> ResponseCode success(T data) {
        return new ResponseCode<>(StatusEnums.SUCCESS, data);
    }

    /** * 返回成功信息 * @return */
    public static ResponseCode success() {
        return new ResponseCode(StatusEnums.SUCCESS);
    }

    /** * 返回错误信息 * @param statusEnums 响应码 * @return */
    public static ResponseCode error(StatusEnums statusEnums) {
        return new ResponseCode(statusEnums);
    }
}
复制代码

八、Shiro配置

ShiroConfig.java

@Configuration
public class ShiroConfig {

	/** * 路径过滤规则 * @return */
	@Bean
	public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		// 若是不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
		shiroFilterFactoryBean.setLoginUrl("/login");
		shiroFilterFactoryBean.setSuccessUrl("/");
		// 拦截器
		Map<String, String> map = new LinkedHashMap<>();
		// 配置不会被拦截的连接 顺序判断
		map.put("/login", "anon");
		// 过滤链定义,从上向下顺序执行,通常将/**放在最为下边
		// 进行身份认证后才能访问
		// authc:全部url都必须认证经过才能够访问; anon:全部url都均可以匿名访问
		map.put("/**", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
		return shiroFilterFactoryBean;
	}

	/** * 自定义身份认证Realm(包含用户名密码校验,权限校验等) * @return */
	@Bean
	public AuthRealm authRealm() {
		return new AuthRealm();
	}

	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(authRealm());
		return securityManager;
	}

	/** * 开启Shiro注解模式,能够在Controller中的方法上添加注解 * @param securityManager * @return */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}
}
复制代码

扩展:权限拦截Filter的URL的一些说明

这里扩展一下权限拦截Filter的URL的一些说明

一、URL匹配规则

(1)“?”:匹配一个字符,如”/admin?”,将匹配“ /admin1”、“/admin2”,但不匹配“/admin”

(2)“”:匹配零个或多个字符串,如“/admin”,将匹配“ /admin”、“/admin123”,但不匹配“/admin/1”

(3)“”:匹配路径中的零个或多个路径,如“/admin/”,将匹配“/admin/a”、“/admin/a/b”

二、shiro过滤器

Filter 解释
anon 无参,开放权限,能够理解为匿名用户或游客
authc 无参,须要认证
logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
authcBasic 无参,表示 httpBasic 认证
user 无参,表示必须存在用户,当登入操做时不作检查
ssl 无参,表示安全的URL请求,协议为 https
perms[user] 参数可写多个,表示须要某个或某些权限才能经过,多个参数时写 perms["user, admin"],当有多个参数时必须每一个参数都经过才算经过
roles[admin] 参数可写多个,表示是某个或某些角色才能经过,多个参数时写 roles["admin,user"],当有多个参数时必须每一个参数都经过才算经过
rest[user] 根据请求的方法,至关于 perms[user:method],其中 method 为 post,get,delete 等
port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

经常使用的主要就是 anon,authc,user,roles,perms 等

注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组受权过滤器,要经过受权过滤器,就先要完成登录认证操做(即先要完成认证才能前去寻找受权) 才能走第二组受权器(例如访问须要 roles 权限的 url,若是尚未登录的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url )。

九、自定义Realm

主要继承AuthorizingRealm,重写里面的方法doGetAuthorizationInfodoGetAuthenticationInfo

受权:doGetAuthorizationInfo

认证:doGetAuthenticationInfo

AuthRealm.java

public class AuthRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Resource
    private RoleService roleService;

    @Resource
    private PermissionService permissionService;

    /** * 受权 * @param principalCollection * @return */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 根据用户Id查询角色信息
        List<Role> roleList = roleService.findRoleByUserId(user.getId());
        Set<String> roleSet = new HashSet<>();
        List<Integer> roleIds = new ArrayList<>();
        for (Role role : roleList) {
            roleSet.add(role.getRole());
            roleIds.add(role.getId());
        }
        // 放入角色信息
        authorizationInfo.setRoles(roleSet);
        // 放入权限信息
        List<String> permissionList = permissionService.findByRoleId(roleIds);
        authorizationInfo.setStringPermissions(new HashSet<>(permissionList));

        return authorizationInfo;
    }

    /** * 认证 * @param authToken * @return * @throws AuthenticationException */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authToken;
        // 根据用户名查询用户信息
        User user = userService.findByAccount(token.getUsername());
        if (user == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }
}
复制代码

十、Contrller层

@RestController
public class LoginController {

    /** * 登陆操做 * @param user * @return */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ResponseCode login(@RequestBody User user) {
        Subject userSubject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
        try {
            // 登陆验证
            userSubject.login(token);
            return ResponseCode.success();
        } catch (UnknownAccountException e) {
            return ResponseCode.error(StatusEnums.ACCOUNT_UNKNOWN);
        } catch (DisabledAccountException e) {
            return ResponseCode.error(StatusEnums.ACCOUNT_IS_DISABLED);
        } catch (IncorrectCredentialsException e) {
            return ResponseCode.error(StatusEnums.INCORRECT_CREDENTIALS);
        } catch (Throwable e) {
            e.printStackTrace();
            return ResponseCode.error(StatusEnums.SYSTEM_ERROR);
        }
    }


    @GetMapping("/login")
    public ResponseCode login() {
        return ResponseCode.error(StatusEnums.NOT_LOGIN_IN);
    }

    @GetMapping("/auth")
    public String auth() {
        return "已成功登陆";
    }

    @GetMapping("/role")
    @RequiresRoles("vip")
    public String role() {
        return "测试Vip角色";
    }

    @GetMapping("/permission")
    @RequiresPermissions(value = {"add", "update"}, logical = Logical.AND)
    public String permission() {
        return "测试Add和Update权限";
    }

    /** * 登出 * @return */
    @GetMapping("/logout")
    public ResponseCode logout() {
        getSubject().logout();
        return ResponseCode.success();
    }
}
复制代码

4、测试

一、登陆:http://localhost:8081/login

{
	"account":"123",
	"password":"232"
}
复制代码

二、其余的是get请求,直接发URL就行啦。

已经过接口测试,你们可放心食用。

参考:juejin.im/post/5d27db…

推荐阅读:

张开涛老的《跟我学Shiro》jinnianshilongnian.iteye.com/blog/201893…


To be continued

做者:鸟不拉屎 出处: https://juejin.im/user/5b3de9155188251aa0161fe4

本文版权归做者和掘金共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利。若是以为还有帮助的话,能够点一下左上角的【点赞】。

相关文章
相关标签/搜索