1.簡介
Apache Shiro是Java的一個安全框架,可用於用認證,授權,加密,會話管理等多個方面,其基本功能點如下圖所示:
模塊 | 用途 |
---|---|
Authenication | 身份認證/登錄,驗證用戶是否擁有相應身份 |
Authorization | 授權,權限驗證,驗證某個用戶是否擁有某個權限 |
Session Manager | 會話管理 |
Cryptography | 加密 |
Web Support | web支持 |
Caching | 緩存 |
Concurrency | 支持多線程應用併發驗證 |
Shiro工作流程如下圖所示:
應用程序交互主體爲Subject,其中Subject可以看做門面,接收請求後實際交給SecurityManager進行處理,SecurityManager爲具體驗證邏輯,而Realm爲數據源來源,可以看做db
2.SpringBoot整合Shiro
-
數據庫準備
-- 用戶token表 create table shiro.user_token ( user_id bigint not null primary key, token varchar(100) not null comment 'token', expire_time datetime null comment '過期時間', update_time datetime null comment '更新時間', constraint token unique (token) ) -- 用戶表 create table shiro.user ( user_id bigint auto_increment primary key, user_name varchar(32) not null comment '用戶名', password varchar(64) not null comment '用戶密碼', mobile varchar(32) null comment '手機號', salt varchar(64) null comment '鹽', locked smallint(6) null comment '是否被鎖定(0:鎖定,1:正常)', create_user_id bigint null comment '創建人', create_time datetime default CURRENT_TIMESTAMP null comment '創建時間', email varchar(32) null, constraint user_user_name_uindex unique (user_name) ) comment '用戶表'; -- 角色表 create table shiro.role ( role_id bigint auto_increment comment '權限id' primary key, role_name varchar(32) not null, remark varchar(64) null comment '權限說明', create_user_id bigint not null comment '創建者id', create_time datetime default CURRENT_TIMESTAMP null comment '創建時間 ' ) comment '權限表'; -- 用戶角色表 create table shiro.user_role ( id bigint auto_increment primary key, user_id bigint not null, role_id bigint not null ) comment '用戶權限中間表'; -- 菜單表 create table shiro.menu ( menu_id bigint auto_increment primary key, name varchar(32) not null comment '菜單名', parent_id bigint not null comment '父菜單id,頂級菜單爲0', perms varchar(32) null comment '權限集合,多個以","分割', parent_name varchar(32) null ) comment '菜單表'; -- 用戶菜單表 create table shiro.role_menu ( id bigint auto_increment primary key, menu_id bigint not null, role_id bigint not null )@Configuration public class FilterConfig { @Bean public FilterRegistrationBean shiroFilterRegistration(){ FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); //該值缺省爲false,表示生命週期由SpringApplicationContext管理,設置爲true則表示由ServletContainer管理 registration.addInitParameter("targetFilterLifecycle", "true"); registration.setEnabled(true); registration.setOrder(Integer.MAX_VALUE - 1); registration.addUrlPatterns("/*"); return registration; } } comment '權限菜單表';
-
數據準備
-- 創建初始用戶 INSERT INTO shiro.user (user_id, user_name, password, mobile, salt, locked, create_user_id, create_time, email) VALUES (1, 'admin', 'da73d08539c91cdf2f11da4f6dcc5dbc2dd9dc8e1647cf1520e3540e77dc2c3c', '15123625205', '932c2d40-ea59-11e9-a2ad-0235d2b38928', 1, 1, '2019-10-09 05:59:51', null); -- 創建菜單權限 INSERT INTO shiro.menu (menu_id, name, parent_id, perms, parent_name) VALUES (1, '新增', 2, 'sys:user:save,sys:role:select', null);
-
springboot環境搭建
springboot基本搭建很簡單,在此就不在搭建,只列出所需的相關依賴:
spring-boot-starter-aop
,lombok
,druid-spring-boot-starter
,mysql-connector-java
,mybatis-plus-boot-starter
,joda-time
,cos_api
-
引入
shiro
依賴<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
-
shiro相關配置
(1)
shiroConfig
:主要用於設置securityManager
及請求攔攔截配置過濾器@Configuration public class ShiroConfig { @Bean("securityManager") public SecurityManager securityManager(ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm); securityManager.setRememberMeManager(null); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); //shiro過濾 Map<String, Filter> filters = new HashMap<>(); filters.put("shiro", new ShiroFilter()); shiroFilter.setFilters(filters); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/webjars/**", "anon"); filterMap.put("/druid/**", "anon"); filterMap.put("/app/**", "anon"); filterMap.put("/sys/login", "anon"); filterMap.put("/swagger/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/captcha.jpg", "anon"); filterMap.put("/aaa.txt", "anon"); filterMap.put("/**", "shiro"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
(2)
shiroFilter
:自定義過濾器該過濾器主要定義了請求成功,或登錄失敗後的處理措施
public class ShiroFilter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isEmpty(token)) { return null; } return new ShiroToken(token); } private String getRequestToken(HttpServletRequest request) { //從header中獲取token String token = request.getHeader("token"); //如果header中不存在token,則從參數中獲取token if (StringUtils.isEmpty(token)) { token = request.getParameter("token"); } return token; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { String token = getRequestToken((HttpServletRequest) servletRequest); if (StringUtils.isEmpty(token)) { HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = new Gson().toJson(ExtResult.error(HttpStatus.SC_UNAUTHORIZED, "invalid token")); httpResponse.getWriter().print(json); return false; } return executeLogin(servletRequest, servletResponse); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { //處理登錄失敗的異常 Throwable throwable = e.getCause() == null ? e : e.getCause(); ExtResult r = ExtResult.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage()); String json = new Gson().toJson(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } }
(3)
shiorToken
:用於自定義token,後續所有請求均依賴於token校驗public class ShiroToken implements AuthenticationToken { private String token; public ShiroToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
(4) token生成器,用於生成token
public class TokenGenerator { public static String generateValue() { return generateValue(UUID.randomUUID().toString()); } public static String generateValue(String param) { try { //指定算法,初始化對象 Provider[] providers = Security.getProviders(); System.out.println(providers); MessageDigest md5 = MessageDigest.getInstance("MD5"); //重置摘要 md5.reset(); //處理數據 md5.update(param.getBytes()); //調用digest將MessageDigest設置成初始化狀態,digest()只能調用一次,因此要初始化 byte[] digest = md5.digest(); return toHexString(digest); } catch (Exception e) { throw new ExtException("生成token失敗", e); } } private static final char[] hexCode = "0123456789abcdef".toCharArray(); private static String toHexString(byte[] data) { if(Objects.isNull(data)){ return null; } StringBuilder sb = new StringBuilder(data.length * 2); for (byte datum : data) { //對每個數字做位運算,只有都爲1時結果才爲1,否則爲0,然後取出數組中對應下標的數 sb.append(hexCode[(datum>>4)& 0xF]); sb.append(hexCode[datum& 0xF]); } return sb.toString(); } public static void main(String[] args) { generateValue(); } }
(5)
shiroRealm
:自定義realm,即通過該realm實現用戶的認證及授權@Component public class ShiroRealm extends AuthorizingRealm { @Resource private ShiroService shiroService; @Override public boolean supports(AuthenticationToken token) { return token instanceof ShiroToken; } /** * @description:用戶授權 <br> * @date: 19-10-9 上午11:43 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //獲取用戶信息 UserDO userDO = (UserDO) principals.getPrimaryPrincipal(); //獲取用戶權限列表 Set<String> userPermission = shiroService.getUserPermission(userDO.getUserId()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(userPermission); return info; } /** * @description:用戶認證 <br> * @date: 19-10-9 上午11:43 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //獲取用戶傳入token String accessToken = (String) token.getPrincipal(); //根據token查詢用戶信息 UserTokenDO userTokenDO = shiroService.queryByToken(accessToken); if (Objects.isNull(userTokenDO) || userTokenDO.getExpireTime().getTime() < System .currentTimeMillis()) { throw new IncorrectCredentialsException("token失效,請重新登錄"); } //根據用戶id查詢用戶 UserDO userDO = shiroService.queryUser(userTokenDO.getUserId()); if(Objects.equals(userDO.getLocked(), 0)){ throw new LockedAccountException("賬號被鎖定,請聯繫管理員"); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDO, accessToken, getName()); return info; } }
(5)配置整體過濾器
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean shiroFilterRegistration(){ FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); //該值缺省爲false,表示生命週期由SpringApplicationContext管理,設置爲true則表示由ServletContainer管理 registration.addInitParameter("targetFilterLifecycle", "true"); registration.setEnabled(true); registration.setOrder(Integer.MAX_VALUE - 1); registration.addUrlPatterns("/*"); return registration; } }
-
測試校驗
(1)驗證用戶登錄,創建token
//實體類 @Data public class LoginForm { private String username; private String password; private String uuid; } //請求controller @PostMapping("/sys/login") public Map<String, Object> login(@RequestBody LoginForm form) throws IOException { //校驗驗證碼,省略 //賬號密碼校驗 UserDO user = userService.queryByUserName(form.getUsername()); if (Objects.isNull(user) || !Objects .equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex(), user.getPassword())) { return ExtResult.error("賬戶或密碼不正確"); } //賬號鎖定 if (Objects.equals(user.getLocked(), 0)) { return ExtResult.error("賬號已被鎖定,請聯繫管理員"); } //生成token,併入庫 ExtResult r = userTokenService.createToken(user.getUserId()); return r; } } ----------------------------------------------------------------------------- //生成token主要邏輯 private final static int EXPIRE = 3600 * 12; @Override @Transactional public ExtResult createToken(long userId) { //生成一個token String token = TokenGenerator.generateValue(); Date now = new Date(); Date exprise = new Date(now.getTime() + EXPIRE * 1000); UserTokenDO userTokenDO = this.getById(userId); //如果token不存在,則生成新的token if(Objects.isNull(userTokenDO)){ userTokenDO = new UserTokenDO(); userTokenDO.setUserId(userId); userTokenDO.setToken(token); userTokenDO.setExpireTime(exprise); userTokenDO.setUpdateTime(now); this.saveOrUpdate(userTokenDO); } //如果存在,則更新token,並更新過期時間 else{ userTokenDO.setUpdateTime(now); userTokenDO.setExpireTime(exprise); userTokenDO.setToken(token); this.updateById(userTokenDO); } return ExtResult.ok().put("token", token).put("exprise", exprise); }
(2)新增用戶
@PostMapping("/save") @RequiresPermissions("sys:user:save") public ExtResult save(@RequestBody UserDO user){ ValidatorUtils.validateEntity(user, AddGroup.class); user.setCreateUserId(getUserId()); //密碼加鹽 user.setPassword(MD5Utils.encrypt(user.getPassword(), user.getSalt())); userService.save(user); return ExtResult.ok(); } ----------------------------------------------------------------------------- // ValidatorUtils:用於校驗傳入參數 public static void validateEntity(Object object, Class<?>... groups) throws ExtException { Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); if (!constraintViolations.isEmpty()) { StringBuilder msg = new StringBuilder(); for(ConstraintViolation<Object> constraint: constraintViolations){ msg.append(constraint.getMessage()).append("<br>"); } throw new ExtException(msg.toString()); } }
(3)實現步驟
- 用戶登錄根據用戶名查詢出用戶信息,校驗密碼正確,校驗賬號狀態是否正常
- 新建用戶時,傳入當前登錄用戶token,首先進入shiroRealm(doGetAuthenticationInfo)校驗用戶是否登錄,token是否有效,如果校驗成功進入下一步
- 上一步校驗成功後,進入回調方法(doGetAuthorizationInfo),根據傳入用戶查詢該用戶權限列表,如果權限列表中包含@RequiresPermissions(“sys:user:save”),指定的權限,則繼續執行後續操作。
3.參考資料
- 開源項目
renren-fast
- 跟我學shiro