天天看點

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

一、前言

SpringBoot+Shiro+Mybatis完成的。

之前看了一位小夥伴的Shiro教程,跟着做了,遇到蠻多坑的(´இ皿இ`)

修改整理了一下,成功跑起來了。可以通過postman進行測試

不多比比∠( ᐛ 」∠)_,直接上源碼:https://github.com/niaobulashi/spring-boot-learning/tree/master/spring-boot-20-shiro

二、Shiro是啥

Apache Shiro是一個功能強大、靈活的、開源的安全架構。可以幹淨利落地處理身份驗證、授權、企業會話管理和加密。

二、Shiro可以幹啥

  • 驗證使用者身份
  • 使用者通路權限控制,比如:1、判斷使用者是否配置設定了一定的安全角色。2、判斷使用者是否被授予完成某個操作的權限
  • 在非 Web 或 EJB 容器的環境下可以任意使用 Session API
  • 可以響應認證、通路控制,或者 Session 生命周期中發生的事件
  • 可将一個或以上使用者安全資料源資料組合成一個複合的使用者 “view”(視圖)
  • 支援單點登入(SSO)功能
  • 支援提供“Remember Me”服務,擷取使用者關聯資訊而無需登入

Shiro架構圖如下:

Spring Boot2(十二):手摸手教你搭建Shiro安全架構
  • Authentication(認證):使用者身份識别,通常被稱為使用者“登入”
  • Authorization(授權):通路控制。比如某個使用者是否具有某個操作的使用權限。
  • Session Management(會話管理):特定于使用者的會話管理,甚至在非web 或 EJB 應用程式。
  • Cryptography(加密):在對資料源使用加密算法加密的同時,保證易于使用。

在概念層,Shiro架構包含三個主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些元件如何互相作用,我們将在下面依次對其進行描述。

  • Subject:目前使用者,Subject 可以是一個人,但也可以是第三方服務、守護程序帳戶、時鐘守護任務或者其它–目前和軟體互動的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合内部安全元件共同組成安全傘。
  • Realms:用于進行權限資訊的驗證,我們自己實作。Realm 本質上是一個特定的安全 DAO:它封裝與資料源連接配接的細節,得到Shiro 所需的相關的資料。在配置 Shiro 的時候,你必須指定至少一個Realm 來實作認證(authentication)和/或授權(authorization)。

三、代碼實作

1、添加Maven依賴

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
<version>1.4.1</version>           

2、配置檔案

application.yml

# 伺服器端口
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);
    }
}           

3、簡單的表設計

無非就是5張表:使用者表、角色表、權限表、使用者角色表、角色權限表。

看下面這張圖,可以說相當明了了。

具體我就不貼出來了,太占篇幅。。直接貼連結:https://github.com/niaobulashi/spring-boot-learning/blob/master/spring-boot-20-shiro/db/test.sql

4、實體類

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;
}           

5、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>           

6、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);
    }
}           

7、系統統一傳回狀态枚舉和包裝方法

狀态字段枚舉

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);
    }
}           

8、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的一些說明

1、URL比對規則

(1)“?”:比對一個字元,如”/admin?”,将比對“ /admin1”、“/admin2”,但不比對“/admin”

(2)“”:比對零個或多個字元串,如“/admin”,将比對“ /admin”、“/admin123”,但不比對“/admin/1”

(3)“”:比對路徑中的零個或多個路徑,如“/admin/”,将比對“/admin/a”、“/admin/a/b”

2、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 )。

9、自定義Realm

主要繼承

AuthorizingRealm

,重寫裡面的方法

doGetAuthorizationInfo

doGetAuthenticationInfo

授權:

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());
    }
}           

10、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();
    }
}           

四、測試

1、登入:http://localhost:8081/login

{
    "account":"123",
    "password":"232"
}           

2、其他的是get請求,直接發URL就行啦。

已認證接口測試,大家可放心食用。

推薦閱讀:

張開濤老的《跟我學Shiro》https://jinnianshilongnian.iteye.com/blog/2018936