在shiro包中新建CustomUsernamePasswordToken类,继承自UsernamePasswordToken。覆写该类的2个方法:getPrincipal方法、getCredentials方法。
public class CustomUsernamePasswordToken extends UsernamePasswordToken() {
private String token;
public CustomUsernamePasswordToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal(){
return token;
}
@Override
public Object getCredentials(){
return token;
}
}
基本流程:
1:通过过滤器拦截用户请求接口,从request中获取到用户认证信息(比如token),如果没有获取到用户认证信息的话,返回提示写入到response响应给前端。
2:将前端传来的认证信息(如token)整合成我们自定义的shiro用户认证UsernamePasswordToken;
3:主体提交认证
4:对用户认证抛出的异常进行try catch封装处理用户认证抛出的异常,写入response响应前端。
在shiro包中新建CustomAccessControlFilter类,继承自AccessControlFilter,该类覆写2个方法:isAccessAllowed方法、onAccessDenied方法。
public class CustomAccessControlFilter extends AccessControlFilter{
/** 如果返回true,就流转到下一个链式调用
如果返回false,就会流转到onAccessDenied方法
这里一般我们可以直接返回false,在onAccessDenied里面处理
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception{
return false;
}
/** 如果返回true,就会流转到下一个链式调用
如果返回false,就不会再继续流转了
*/
@override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServvletRequest) servletRequest;
log.info("request 接口地址:{}", request.getRequestURI());
log.info("request 接口的请求方式:{}", request.getMethod());
/**
需要做3步:
1)取出用户认证信息,比如token或者cookie。并进行校验,如果检验错误,则需要返回false,并将提示写入response响应前端
2) 携带了通过验证的认证信息后,则将认证信息整合成步骤1中自定义的 shiro用户认证的UsernamePasswordToken
3)根据shiro用户认证的UserrnamePasswordToken,主体提交认证getSubject
以上3步需根据你的具体的需要进行编写,比如你的用户认证是token,还是cookie,还是session。所以以下代码仅供参考流程。
以下代码中,默认约定好由前端是把认证信息放入request的header中的"TOKEN"字段里面。获取了 token以后,不为空的话就直接封装为自定义的shiro用户认证的customUsernamePasswordToken,最后提交认证。当然如果这个过程中有异常,则将信息写入到response响应给前端。
*/
String accessToken = request.getHeader("TOKEN");
try {
if (StringUtils.isEmpty(accessToken)) {
throw new BusinessException(4010001, "认证token不能为空,请重新登录获取");
}
CustomUsernamePassword customUsernamePasswordToken = new CustomUsernamePasswordToken(accessToken);
getSubject(servletReques,servletResponse).login(customUsernamePasswordToken);
} catch (BusinessException e) {
customResponse(servletResponse, e.getCode(), e.getMsg());
return false;
} catch (AuthenticationException e){
if (e.getCause() instanceof BusinessException) {
BusinessException exception = (BusinessException) e.getCause();
customResponse(servletResponse, exception.getCode(), exception.getMsg());
} else {
customResponse(servletResponse, 4010001, "认证token不能为空,请重新登录获取");
}
return false;
} catch (Exception e) {
customResponse(servletResponse, 5000001, "系统服务异常");
return false;
}
return true;
}
/** 自定义响应前端方法
*/
private void customResponse(ServletResponse response, int code, String msg) {
try {
DataResult result = DataResult.getResult(code, msg);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_AVLUE);
response.setCharacterEncoding("UTF-8");
String str = JSON.toJsonString(rresult);
OutputStream outputStream = response.getOutputStream();
outputStream.write(str.getBytes("UTF-8"));
outputStream.flush();
} catch (IOException e) {
log.error("CustomResponse_error:{}", e);
}
}
}
自定义realm中定义了 用户认证的逻辑,以及通过认证后,用户有哪些角色信息和权限信息,shiro从而来判断该用户 是否可以访问该资源。
1、用户认证业务逻辑:在步骤2的主体提交认证信息后(onAccessDenied中getSubject),会流转到用户认证器(ModularRealmAuthentication),用户认证器会通过自定义域中 的doGetAuthenticationInfo方法获取用户认证凭证
注:用户认证底层代码一开始会通过supports方法判断所传入的AuthenticationToken是否是有效的,因此我们自定义的realm中还需要覆写supports方法。其中shiro里面的doSingleRealmAuthentication的源代码如下:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not suport authentication token [" + token + "]";
throw new UnsupportedTokenException(msg);
} else {
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] wasunable to find account data for the submitted AuthenticationToken [" + token + "]";
throw new UnknownAccountException(msg);
} else {
return info;
}
}
}
2、用户授权的业务逻辑:即主体提交授权后(eg:subject.checkRoles(“admin”),@RequiresPermissions(“role:add”)),授权器(ModularRealmAuthorizer)就会通过自定义域的doGetAuthorizationInfo方法获取该用户所拥有的角色信息、权限信息。然后判断该用户是否有权访问该资源。
在shiro包中新建一个CustomRealm继承自AuthorizingRealm,并且覆写其中3个方法:doGetAuthenticationInfo方法、doGetAuthorizationInfo方法、support方法。
public class CustomRealm extends AuthorizingRealm {
@Override
public boolean supports (AuthenticationToken token) {
return token instanceof CustomUsernamePasswordToken;
}
@Override
protected Authentication Info doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//首先把前端传来的token进行强转
CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken) token;
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(customUsernamePasswordToken.getPrincipal(), customUsernamePasswordToken.getCredentials(), this.getName());
return info;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 注:在本例中,是将用户角色信息和权限信息在用户第一次登录的时候,就已经写入到token里面了,所以可以通过JwtTokenUtils工具类直接取出来放入SimpleAuthorizationInfo中。
// 实际开发中,请根据你自己从前端拿到的数据,来判断该用户有哪些角色信息和权限信息,然后放入SimpleAuthorizationInfo中。
String accessToken = (String) principals.getPrimaryPrincipal();
Claims claimsFromToken = JwtTokenUtils.getClaimsFromToken(accessToken);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (claimsFromToken.get("角色信息") != null) {
info.addStringPermissions((Collection<String> claimsFromToken.get("角色信息")));
}
if (claimsFromToken.get("权限信息") != null) {
info.addStringPermissions((Collection<String> claimsFromToken.get("权限信息")));
}
return info;
}
}
shiro在第3步中获取到AuthenticationInfo之后,还需要判断该用户是否有权限可以登录认证,即执行assertCredentialsMatch方法。然后在assertCredentialsMatch方法中,实际是使用CredentialsMatcher类的doCredentialMatch进行判断是否匹配。但是shiro中自带的doCredentialsMatch方法有时并不能满足我们自定义的需求,因此这时也需要我们再自己自定义重写。以下为shiro中部分源码:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) thorws AuthenticationException {
// 如果缓存中没有Authentication的话,从第3步骤中自定义的doGetAuthenticationInfo中获取,并且存入缓存中
Authentication Info info = getCachedAuthenticationInfo(token);
if (info == null) {
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthnetiocationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
// 获取到doGetAuthenticationInfo后,再通过assertCredentialsMatch方法验证是否匹配
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No Authnetication Info found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
protected void assertCredentialMatch (AuthenticationToken token, AuthenticationInfo info) thorws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialMatch(token, info)) {
String msg = "报错信息,此处省略";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("报错西悉尼,此处省略");
}
}
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
因此我们在shiro包中自定义CustomHashedCredentialsMatcher继承自HashedCredentialsMatcher, 需要覆写其中的1个方法:doCredentialsMatch方法。
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 本例中使用了redis,可根据你的实际情况进行改写
@Autowired
private RedisService redisService;
// 以下为本例的判断token是否有效的逻辑,具体请根据你的需求逻辑来进行编写。
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken) token;
String accessToken = (String) customUsernamePasswordToken.getCredentials();
String userId = JwtTokenUtils.getUserId(accessToken);
log.info("doCredentialsMatch...userId={}", userId);
// 判断用户是否删除
if (redisService.hasKey(Constant.DELETED_USER_KEY + userId)) {
throw new BusinessException(BaseResponseCode.ACCOUNT_HAS_DELETED_ERROR);
}
// 判断用户是否被锁定
if (redisService.hasKey(Constant.ACCOUNT_LOCK_KEY + userId)) {
throw new BusinessException(BaseResponseCode.ACCOUNT_LOCK);
}
// 检验token
if(!JwtTokenUtil.validateToken(accessToken)) {
throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);
}
return true;
}
}
在config包中新建ShiroConfig类进行shiro的策略配置
@Configuration
public class ShiroConfig {
@Bean
public CustomHashedCredentialsMatcher customHashedCredentialsMatcher() {
return new CustomHashedCredentialsMatcher();
}
@Bean
public CustomRealm customRealm() {
CustomRealm customRealm = new CustomRealm();
customRealm.setCredentialsMatcher(customHashedCredentialsMatcher);
return customRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWehbSecurityManager = new DefaultWebSecurityManager();
defaultWehbSecurityManager.setRealm(customRealm());
return defaultWehbSecurityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
LinkedHashMap<String, Filter> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("token", new CustomAccessControlFilter());
shiroFilterFactoryBean .setFilters(linkedHashMap);
// 配置拦截策略
LinkedHashMap<String, String> hashMap = new LinkedHashMap<>();
hashMap.put("/api/usr/login", "anon");
hashMap.put("/swagger/**", "anon");
hashMap.put("/druid/**", "anon");
hashMap.put("/**", "token,authc");
shiroFilterFactoryBean.setFilterEFINITIONMap(hashMap);
return shiroFilterFactoryBean;
}
/** 开启shiro aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}