关于shiro包含session和无状态同时存在时实现思路
以前概念里,关于客户端登录,没有cookieId,需要手动记录保存sessionid。
在看了shiro教程以后,了解了无状态的web应用集成
即服务端无状态,服务器端不会存储像会话这种东西,而是每次请求带上相应的用户名进行登录。
具体实现:
1、教程上讲,客户端每次申请一个token,token一次性,只能用一次。
2、服务器记录下这些token,如果之前用过就是非法请求。
关于token,本文用的是无限制的,可以用很多次。当然,一个token只能使用一次的更为安全。目前想到的,
使用一次的方案为:时间戳加固定字符串进行编码。服务端进行解码验证,
判断时间戳的时效性来判断token是否真实有效。
第一步 创建无状态使用的token
目的,根据token调用不同的relam。
public class NoStatelessToken implements AuthenticationToken {
private String username;
private String params;
.....
}
第二步 写mobileUserRealm 和userRealm
public class MobileUserRealm extends BaseAuthorizingRealm {
//注入用到的所有服务
public MobileUserRealm() {
//根据realm绑定的token不同,登录时自动调用相关realm进行登录
super();
setAuthenticationTokenClass(NoStatelessToken.class);
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
NoStatelessToken noStatelessToken = (NoStatelessToken) token;
String username= noStatelessToken.getusername();
//使用jwt初始化验证得到用户
User user = jwtService.getusername(username);
if (user.getStatus() == Status.DENY || user.getStatus() == Status.DELETE) {
throw new DisabledAccountException("帐号被禁用");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, authToken, getName());
return info;
}
}
public class UserRealm extends BaseAuthorizingRealm {
public UserRealm() {
setAuthenticationTokenClass(UsernamePasswordToken.class);
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
String password = new String((char[]) authenticationToken.getCredentials());
User user = userService.findByPhone(username);
if (user == null) {
throw new MyException("用户不存在");
}
boolean check = true;
if (username.equals(TEST_PHONE) && TEST_CODE.equals(password)) {
check = true;
}
if (!check) {
throw new UnknownAccountException("验证码错误!");
}
if (user.getStatus() == Status.DENY || user.getStatus() == Status.DELETE) {
throw new DisabledAccountException("帐号被禁用");
}
logger.debug("用户{}登录成功", username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
//同步user信息到,这一步可以不必做,上一步登录成功会自动同步。
UserUtil.setUser(user);
return info;
}
}
public abstract class BaseAuthorizingRealm extends AuthorizingRealm {
//授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
if (principalCollection == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
User user = (User) principalCollection.getPrimaryPrincipal();
User one = userService.get(user.getId());
List<String> roleList = new ArrayList<>();
if(one.getStatus() == Status.ALLOW){
roleList = roleService.listRoleSnByUser(user.getId(), Status.ALLOW.getStatus());
}
logger.info("----BaseAuthorizingRealm + roleList----"+roleList);
List<String> authorityList = new ArrayList<>();
List<Integer> list = roleService.listIdByUserIdAndFirstDepIdAndUserType(user.getId(), user.getFirstDepId(),Status.ALLOW.getStatus(),user.getUserType());
if (list != null && list.size() > 0) {
for (Integer integer : list) {
List<String> params = authorityService.listParamsByRoleId(integer, Status.ALLOW.getStatus());
authorityList.addAll(params);
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(new HashSet<>(roleList));
info.setStringPermissions(new HashSet<>(authorityList));
return info;
}
//清除权限缓存
public void refreshCache(RolePermissionEvent event){
switch (event.getType()){
case PERMISSION:
Cache<Object, AuthorizationInfo> authorizationCache = getAuthorizationCache();
if(authorizationCache != null){
getAuthorizationCache().clear();
}
logger.info("------------清除全部用户权限----------"+authorizationCache);
break;
case USER:
User user = userService.get(event.getUserId());
String realmName = getName();
PrincipalCollection principals = new SimplePrincipalCollection(user, realmName);
if(principals != null){
clearCachedAuthorizationInfo(principals);
}
logger.info("------------清除单个用户权限----------"+user);
break;
default:
break;
}
}
}
第三步重写sessionfactory
public class MyselfSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
//没有经过shirofilter的请求,无法强转为webSubjectContext。涉及到shiro自带的包装
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
}
WebSubjectContext webSubjectContext = (WebSubjectContext)context;
HttpServletRequest request = (HttpServletRequest) webSubjectContext.getServletRequest();
//登录状态下,不创建session不保存用户信息。 如果不加这个,会导致request为null,这个有疑点,探究后再来完善。
if (!context.isAuthenticated()) {
if (UserAgentUtil.isClient( request)) {
//不创建session
context.setSessionCreationEnabled(false);
} else {
//创建session
context.setSessionCreationEnabled(true);
}
}
return super.createSubject(context);
}
第四步 创建拦截请求的filter
目的,区分开pc和客户端,pc还是用最基础的有session的方式,用浏览器自带cookie存储session实现用户信息会话范围内共享。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//手机端进行拦截,pc端直接放过
if (UserAgentUtil.isClient((HttpServletRequest) request))) {
//判断该请求是否登录可选,将其放入request中以做后续处理
request.setAttribute(OPTIONAL,mappedValue==null?false:true);
// if (mappedValue!=null){
// return true;
// }
//进入下一步处理
return false;
} else {
return true;
}
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//业务处理,解析token
//根据token信息拿到用户信息
//进行subject.login 登录 NoStatelessToken 是自己定义的token,定义这个token为了区分开pc的usernamepasswordtoken,
//然后调用不同的realm
getSubject(request, response).login(new NoStatelessToken(token, siteDto));
}
第五步 配置shiro.xml
shiro基本配置不再重复,新增的配置展示如下:
<bean id ="subjectFactory" class="自己项目路径的MyselfSubjectFactory"/>
<bean id="statelessAuthcFilter" class="自己项目路径的MobileFilter"/>
<bean id="userRealm" class="自己项目路径的UserRealm">
<!--<property name="rolePermissionResolver" ref="userRolePermissionResolver"/>-->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<bean id="mobielUserRealm" class="自己项目路径的MobileUserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="authorizationCachingEnabled" value="false"/>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="userRealm"/>
<ref bean="mobielUserRealm"/>
</list>
</property>
<property name="subjectDAO">
//稍后解释,为什么重写sessionStorageEvaluator
<bean class="org.apache.shiro.mgt.DefaultSubjectDAO">
<property name="sessionStorageEvaluator" ref="customeSessionStorge"/>
</bean>
</property>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="redisCacheManager"/>
<property name="subjectFactory" ref="subjectFactory"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="filters">
<util:map>
<entry key="statelessAuthc" value-ref="statelessAuthcFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
# some example chain definitions:
# more URL-to-FilterChain definitions here
/m = anon
/news/list = statelessAuthc[optional]
# 所有过shirofilter的请求,都走pc和客户端的验证 authc 默认验证是userrealm
/** = statelessAuthc,authc
</value>
</property>
</bean>
配置请求用的/**方式,使用下来觉得不妥,还是给方法加统一前缀,这样配置起来更简单明了。
第六步 配置web.xml
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
为什么我要把web.xml的配置贴出来,是因为,这个shiroFilter拦截的所有的请求,项目路径下面的静态文件也过了shirofilter,造成静态资源也创建了subject,还是能创建session的subject,并且缓存了用户的登录后 的信息。
重写的sessionStorge
在项目里只用到无状态的情况下,这个可以参考shiro教程的方法,配置shiro.xml即可,但是有两种情况时既有无状态,又有有session的,
需要重写区分修改配置文件如下:
//配置文件的方式
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="statelessRealm"/>
//在这里设置
<property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled" value="false"/>
<property name="subjectFactory" ref="subjectFactory"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
//重写sessionstorage的方式
public class CustomeSessionStorge extends DefaultWebSessionStorageEvaluator{
@Override
public boolean isSessionStorageEnabled(Subject subject) {
if(subject instanceof WebSubject){
HttpServletRequest request = (HttpServletRequest) ((WebSubject) subject).getServletRequest();
if (UserAgentUtil.isClient( request) ) {
return false;
}
}
return super.isSessionStorageEnabled(subject);
}
}
贴下源码,默认的是如何存储的。。。app不带token时却能拿到身份信息,原因就在这里,存储了用户信息。
@SuppressWarnings({"SimplifiableIfStatement"})
@Override
public boolean isSessionStorageEnabled(Subject subject) {
if (subject.getSession(false) != null) {
//use what already exists
return true;
}
//没有session,切为false的时候才不存储用户身份信息,实则一一个坑。
if (!isSessionStorageEnabled()) {
//honor global setting:
return false;
}
//SHIRO-350: non-web subject instances can't be saved to web-only session managers:
//since 1.2.1:
if (!(subject instanceof WebSubject) && (this.sessionManager != null && !(this.sessionManager instanceof NativeSessionManager))) {
return false;
}
return WebUtils._isSessionCreationEnabled(subject);
}