先说说项目中的实际需求。首先项目是有shiro的,也有一套完整的登录认证系统,能够实现使用缓存/dao进行登录用户名密码的校验。然后最近有一个需求,需要实现手机端的登录,登录的请求链接与web端不同,验证是由另外一台服务器进行验证的,使用webservice进行通信。
总结一下,也就是说需要实现一套使用WebService的登录接口。
我使用的解决方案是开放一个无需验证的登录接口给app端使用,在这个接口中调用登录的方法,并且使用webService去进行用户名密码的校验。
1、修改配置文件
在shiro的配置文件中,增加无需验证的登录接口
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
${adminPath}/app/user/login = anon
${adminPath}/app/user/notAuthorized = user
${adminPath}/app/user/sessionTimeout = anon
</value>
</constructor-arg>
</bean>
2、自定义的Token
/** * 自定义的移动端登录token,因为用户名和密码不需要本系统进行校验,所以只需要保存是否校验通过和校验通过后的用户id */ public class AppToken extends AuthenticationToken { private static final long serialVersionUID = 8587329689973009518L; private String checked = null; //webService的校验是否通过 private String userId = null; //登录用户的id private boolean isRememberMe = false; //是否勾选记住我 public AppToken(String userId, String ticket) { this.userId = userId; this.checked = ticket; } /** * 获取当前token的身份,即用户id * @return */ public Object getPrincipal() { return this.userId; } /** * 获取当前用户的凭证,即是否认证通过 * @return */ public Object getCredentials() { return this.checked; } public void setUserId(String userId) { this.userId = userId; } public boolean isRememberMe() { return this.isRememberMe; } public void setRememberMe(boolean isRememberMe) { this.isRememberMe = isRememberMe; } }
3、自定义的Realm,并且加入到配置文件中的配置中去
/** * 自定义的移动端认证器,并不进行任何的认证工作,只是为当前用户处理session */ @Service public class AppRealm extends AbstractCustomRealm { private SystemService systemService; private SystemAuthorizingRealm realm; private String name; private Logger logger = Logger.getLogger(getClass()); @Override public String getName() { return this.name; } @Override public boolean supports(AuthenticationToken authenticationToken) { return authenticationToken!=null; } @Override public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (! (authenticationToken instanceof AppToken)) return null;//只有通过app登录接口进来的请求才能使用该认证器 AppToken token = (AppToken)authenticationToken; Object principal = token.getPrincipal(); //获取token中的身份,是用户的id User user = getSystemService().getUser((String) principal);//从系统缓存中获取当前用户的信息 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(new SystemAuthorizingRealm.Principal(user, false),token.getCredentials(),user.getLoginName());//创建Info对象,并不进行校验 return authenticationInfo; }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return getRealm().doGetAuthorizationInfo(principalCollection); } private SystemService getSystemService() { if (systemService == null){ systemService = SpringContextHolder.getBean(SystemService.class); } return systemService; } private SystemAuthorizingRealm getRealm() { if(realm == null){ realm = SpringContextHolder.getBean(SystemAuthorizingRealm.class); } return realm; }
加入配置文件
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean = "appRealm"/> <ref bean = "systemAuthorizingRealm"/> </list> </property> <property name="sessionManager" ref="sessionManager" /> <property name="cacheManager" ref="shiroCacheManager" /> <property name="authenticator" ref="authenticator"/> </bean>
4、完成接口,在接口中对用户名和密码进行处理
@Controller @RequestMapping(value = "${adminPath}/" + AppConst.AppUser_Action) public class AppUserController extends BaseController { @Autowired private SystemService systemService; private String oaWsUrl = "test" @ResponseBody @RequestMapping(value = "login") public String login(UserLoginVo loginVo) {
Result res = new Result();
try { AppToken phoneToken = null; boolean app = false; User user = null; String result = UserAuthWsClient.authUserOAWS(loginVo.getUsername(), loginVo.getPassword(), oaWsUrl); String[] results = result.split("-"); if (!"0".equals(results[0])){ if (results.length > 1) { logger.error(results[1]); } logger.debug("移动端登录(userid: "+loginVo.getUsername()+")登录失败."); res.setMessage("用户名或者密码错误,请重新输入"); res.setCode(ResponseVo.FAIL); return res; } if ("0".equals(results[0])) { String JC = loginVo.getUsername() ; user = UserUtils.get(JC); if (user != null) { if (user.getDelFlag() == 0) { app = true; } } if(app){ phoneToken = new AppToken(user.getId(), "checked"); } } if(!app){ //本系统中没有该用户,判断登录失败 res.setMessage("登录失败, 请联系管理员"); res.setCode(ResponseVo.FAIL); }else{ Subject subject = SecurityUtils.getSubject(); try { UserUtils.clearCache(); subject.login(phoneToken); logger.debug("移动端登录(userid: "+user.getId()+") 登录成功,"); res.setMessage("登陆成功"); res.setBody(loginVo); res.setCode(ResponseVo.SUCCESS); } catch (AuthenticationException e) { logger.debug("移动端登录(userid: "+user.getId()+")登录失败."); logger.error(e.getMessage(),e); res.setMessage("用户名或者密码错误,请重新输入"); res.setCode(ResponseVo.FAIL); } return res; } }
}
}
}
总结一下,因为用户名和密码的校验不在本系统内完成,所以对用户名和密码就不需要传入到shiro相关的流程中,直接使用继承自CachingRealm的自定义认证器AbstractCustomRealm ,在这里面不会对token和info的一致性进行校验,因此就不需要关心账号和密码了。
如果是自定的Realm继承自 AuthorizingRealm,那么AuthorizingRealm还会对token和info的一致性进行校验,只有当校验成功后才算是登录成功,否则抛出异常登录失败