以前項目中權(quán)限認(rèn)證沒有使用安全框架寓娩,都是在自定義filter中判斷是否登錄以及用戶是否有操作權(quán)限的荔泳。
最近開了新項目裁着,搭架子時持钉,想到使用安全框架來解決認(rèn)證問題,spring security太過龐大第练,我們的項目不大阔馋,所以決定采用Shiro
什么是Shiro
Apache Shiro 是一個強(qiáng)大靈活的開源安全框架,可以完全處理身份驗證娇掏、授權(quán)呕寝、加密和會話管理。
Realm是Shiro的核心組建婴梧,也一樣是兩步走下梢,認(rèn)證和授權(quán),在Realm中的表現(xiàn)為以下兩個方法塞蹭。
- 認(rèn)證:doGetAuthenticationInfo孽江,核心作用判斷登錄信息是否正確
- 授權(quán):doGetAuthorizationInfo,核心作用是獲取用戶的權(quán)限字符串番电,用于后續(xù)的判斷
Shiro過濾器
當(dāng) Shiro 被運用到 web 項目時岗屏,Shiro 會自動創(chuàng)建一些默認(rèn)的過濾器對客戶端請求進(jìn)行過濾。以下是 Shiro 提供的部分過濾器:
過濾器 | 描述 |
---|---|
anon | 表示可以匿名使用 |
authc | 表示需要認(rèn)證(登錄)才能使用 |
authcBasic | 表示httpBasic認(rèn)證 |
perms | 當(dāng)有多個參數(shù)時必須每個參數(shù)都通過才通過 perms[“user:add:”] |
port | port[8081] 跳轉(zhuǎn)到schemal://serverName:8081?queryString |
rest | 權(quán)限 |
roles | 角色 |
ssl | 表示安全的url請求 |
user | 表示必須存在用戶钧舌,當(dāng)?shù)侨氩僮鲿r不做檢查 |
為什么選擇shiro
- 簡單性担汤,Shiro 在使用上較 Spring Security 更簡單,更容易理解洼冻。
- 靈活性崭歧,Shiro 可運行在 Web、EJB撞牢、IoC率碾、Google App Engine 等任何應(yīng)用環(huán)境,卻不依賴這些環(huán)境屋彪。而 Spring Security 只能與 Spring 一起集成使用所宰。
- 可插拔,Shiro 干凈的 API 和設(shè)計模式使它可以方便地與許多的其它框架和應(yīng)用進(jìn)行集成畜挥。Shiro 可以與諸如 Spring仔粥、Grails、Wicket蟹但、Tapestry躯泰、Mule、Apache Camel华糖、Vaadin 這類第三方框架無縫集成麦向。Spring Security 在這方面就顯得有些捉衿見肘。
spring boot整合shiro
添加maven依賴
在項目中引入shiro非常簡單客叉,我們只需要引入 shiro-pring 就可以了
<!-- SECURITY begin -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- SECURITY end -->
shiro自定義認(rèn)證token
AuthenticationToken 用于收集用戶提交的身份(如用戶名)及憑據(jù)(如密碼)诵竭。Shiro會調(diào)用CredentialsMatcher對象的doCredentialsMatch方法對AuthenticationInfo對象和AuthenticationToken進(jìn)行匹配话告。匹配成功則表示主體(Subject)認(rèn)證成功,否則表示認(rèn)證失敗卵慰。
Shiro 僅提供了一個可以直接使用的 UsernamePasswordToken沙郭,用于實現(xiàn)基于用戶名/密碼主體(Subject)身份認(rèn)證。UsernamePasswordToken實現(xiàn)了 RememberMeAuthenticationToken 和 HostAuthenticationToken呵燕,可以實現(xiàn)“記住我”及“主機(jī)驗證”的支持棠绘。
我們的業(yè)務(wù)邏輯是每次調(diào)用接口件相,不使用session存儲登錄狀態(tài)再扭,使用在head里面存token的方式,所以不使用session夜矗,并不需要用戶密碼認(rèn)證泛范。
自定義token如下:
/**
* Created by Youdmeng on 2020/6/24 0024.
*/
public class YtoooToken implements AuthenticationToken {
private String token;
public YtoooToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
shiro自定義Realm
Realm是shiro的核心組件,主要處理兩大功能:
- 認(rèn)證 我們接收filter傳過來的token紊撕,并認(rèn)證login操作的token
- 授權(quán) 獲取到登錄用戶信息罢荡,并取得用戶的權(quán)限存入roles,以便后期對接口進(jìn)行操作權(quán)限驗證
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Autowired
private JedisClusterClient jedis;
/**
* 大坑对扶!区赵,必須重寫此方法,不然Shiro會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof YtoooToken;
}
/**
* 授權(quán)
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("Shiro權(quán)限配置");
String token = principals.toString();
UserDetailVO userDetailVO = JSON.parseObject(jedis.get(token), UserDetailVO.class);
Set<String> roles = new HashSet<>();
roles.add(userDetailVO.getAuthType() + "");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
return info;
}
/**
* 認(rèn)證
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("Shiror認(rèn)證");
YtoooToken usToken = (YtoooToken) token;
//獲取用戶的輸入的賬號.
String sid = (String) usToken.getCredentials();
if (StringUtils.isBlank(sid)) {
return null;
}
log.info("sid: " + sid);
return new SimpleAccount(sid, sid, "userRealm");
}
}
shiro自定義攔截器
自定義shiro攔截器來控制指定請求的訪問權(quán)限浪南,并登錄shiro以便認(rèn)證
我們自定義shiro攔截器主要使用其中的兩個方法:
- isAccessAllowed() 判斷是否可以登錄到系統(tǒng)
- onAccessDenied() 當(dāng)isAccessAllowed()返回false時笼才,登錄被拒絕,進(jìn)入此接口進(jìn)行異常處理
/**
* Created by Youdmeng on 2020/6/24 0024.
*/
@Slf4j
public class TokenFilter extends FormAuthenticationFilter {
private String errorCode;
private String errorMsg;
private static JedisClusterClient jedis = JedisClusterClient.getInstance();
/**
* 如果在這里返回了false络凿,請求onAccessDenied()
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String sid = httpServletRequest.getHeader("sid");
if (StringUtils.isBlank(sid)) {
this.errorCode = ResponseEnum.TOKEN_UNAVAILABLE.getCode();
this.errorMsg = ResponseEnum.TOKEN_UNAVAILABLE.getMessage();
return false;
}
log.info("sid: " + sid);
UserDetailVO userInfo = null;
try {
userInfo = JSON.parseObject(jedis.get(sid), UserDetailVO.class);
} catch (Exception e) {
this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
return false;
}
if (userInfo == null) {
this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
return false;
}
//刷新超時時間
jedis.expire(sid, 30 * 60); //30分鐘過期
YtoooToken token = new YtoooToken(sid);
// 提交給realm進(jìn)行登入骡送,如果錯誤他會拋出異常并被捕獲
getSubject(request, response).login(token);
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
ResponseMessage result = Result.error(this.errorCode,this.errorMsg);
String reponseJson = (new Gson()).toJson(result);
response.setContentType("application/json; charset=utf-8");
response.setCharacterEncoding("utf-8");
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
outputStream.write(reponseJson.getBytes());
} catch (IOException e) {
log.error("權(quán)限校驗異常",e);
} finally {
if (outputStream != null){
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("權(quán)限校驗,關(guān)閉連接異常",e);
}
}
}
return false;
}
}
配置ShiroConfig
springboot中絮记,組件通過@Bean的方式交由spring統(tǒng)一管理摔踱,在這里需要配置 securityManager,shiroFilter怨愤,AuthorizationAttributeSourceAdvisor
注入realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
注入 securityManager
@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
/*
* 關(guān)閉shiro自帶的session派敷,詳情見文檔
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
注入 shiroFilter
此處將自定義過濾器添加到shiro中,并配置具體哪些路徑撰洗,執(zhí)行shiro的那些過濾規(guī)則
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的過濾器并且取名為token
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("token", new TokenFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
/*
* 自定義url規(guī)則
* http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new HashMap<>();
//swagger
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/**/*.js", "anon");
filterRuleMap.put("/**/*.png", "anon");
filterRuleMap.put("/**/*.ico", "anon");
filterRuleMap.put("/**/*.css", "anon");
filterRuleMap.put("/**/ui/**", "anon");
filterRuleMap.put("/**/swagger-resources/**", "anon");
filterRuleMap.put("/**/api-docs/**", "anon");
//swagger
//登錄
filterRuleMap.put("/login/login", "anon");
filterRuleMap.put("/login/verifyCode", "anon");
// 所有請求通過我們自己的JWT Filter
filterRuleMap.put("/**", "token");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
配置DefaultAdvisorAutoProxyCreator
解決 在@Controller注解的類的方法中加入@RequiresRole等shiro注解篮愉,會導(dǎo)致該方法無法映射請求,導(dǎo)致返回404了赵。
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解決一個奇怪的bug潜支。在引入spring aop的情況下。
* 在@Controller注解的類的方法中加入@RequiresRole等shiro注解柿汛,會導(dǎo)致該方法無法映射請求冗酿,導(dǎo)致返回404埠对。
* 加入這項配置能解決這個bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
配置 AuthorizationAttributeSourceAdvisor 使doGetAuthorizationInfo()Shiro權(quán)限配置生效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
在接口中控制權(quán)限
使用RequiresRoles注解來配置該接口需要的權(quán)限
當(dāng)配置logical = Logical.OR時,登錄這配置的權(quán)限在1,2,3中任意一個裁替,既可以成功訪問接口
@ApiOperation("任務(wù)調(diào)度")
@PostMapping("/dispatch")
@RequiresRoles(value = { "1", "2", "3" }, logical = Logical.OR)
public ResponseMessage dispatch(@RequestBody @Valid DispatchVO dispatchVO) {
log.info("任務(wù)調(diào)度開始 入?yún)?" + JSON.toJSONString(dispatchVO));
try {
service.dispatch(dispatchVO);
return Result.success(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage());
} catch (RuntimeException e) {
log.error("任務(wù)調(diào)度失敗", e);
return Result.error(ResponseEnum.ERROR.getCode(), e.getMessage());
} catch (Exception e) {
log.error("任務(wù)調(diào)度失敗", e);
return Result.error(ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
}
}
統(tǒng)一的異常處理
配置全局異常處理
@ControllerAdvice
@Order(value=1)
public class ShiroExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ShiroExceptionAdvice.class);
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler({AuthenticationException.class, UnknownAccountException.class,
UnauthenticatedException.class, IncorrectCredentialsException.class})
@ResponseBody
public ResponseMessage unauthorized(Exception exception) {
logger.warn(exception.getMessage(), exception);
logger.info("catch UnknownAccountException");
return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
}
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public ResponseMessage unauthorized1(UnauthorizedException exception) {
logger.warn(exception.getMessage(), exception);
return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
}
}
上面使用的redis工具
@Bean
@DependsOn("ConfigUtil")
public JedisClusterClient getClient() {
ml.ytooo.redis.RedisProperties.expireSeconds = redisProperties.getExpireSeconds();
ml.ytooo.redis.RedisProperties.clusterNodes = redisProperties.getClusterNodes();
ml.ytooo.redis.RedisProperties.connectionTimeout = redisProperties.getConnectionTimeout();
ml.ytooo.redis.RedisProperties.soTimeout = redisProperties.getSoTimeout();
ml.ytooo.redis.RedisProperties.maxAttempts = redisProperties.getMaxAttempts();
if (StringUtils.isNotBlank(redisProperties.password)) {
ml.ytooo.redis.RedisProperties.password = redisProperties.password;
}else {
ml.ytooo.redis.RedisProperties.password = null;
}
return JedisClusterClient.getInstance();
}
@Data
@Component
@ConfigurationProperties(prefix = "redis.cache")
public class RedisProperties {
private int expireSeconds;
private String clusterNodes;
private int connectionTimeout;
private String password;
private int soTimeout;
private int maxAttempts;
}
依賴工具集:
<dependency>
<groupId>ml.ytooo</groupId>
<artifactId>ytooo-util</artifactId>
<version>3.7.0</version>
</dependency>
收工
更多好玩好看的內(nèi)容项玛,歡迎到我的博客交流,共同進(jìn)步????????WaterMin