一、前言
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框架圖如下:
- 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