基于Shiro的登录功能 设计思路
认证流程
Shiro的认证流程可以看作是个“有窗户黑盒”,
整个流程都有框架控制,对外的入口只有subject.login(token);,这代表“黑盒”
流程中的每一个组件,都可以使用Spring IOC容器里的Bean来替换,以实现拓展化、个性化,这代表“有窗户”。
本示例的认证流程可以参考下图:
(黑色区域中的红箭头线只代表受关注组件的流程,而不是直接调用,实际上Shiro每个流程的组件都是互相解耦的)
黑色区域里的两块白色区域,就是两个自定义的类。
关键代码如下:
/** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); AccountPo po = accountMapper.getByUsername(username); if (po == null) { throw new UnknownAccountException("用户名或密码错误。"); } if (!po.getEnable_flag()) { throw new DisabledAccountException("该用户已被禁用。"); } CredentialsDto credentials = new CredentialsDto(po.getPassword(), po.getSalt()); return new SimpleAuthenticationInfo(username, credentials, getName()); }
/** * 密码匹配 */ @Override public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo info) { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String password = new String(token.getPassword()); CredentialsDto credentials = (CredentialsDto) info.getCredentials(); String salt = credentials.getSalt(); String passwordMd5 = DigestUtils.md5Hex(password + salt); boolean result = equals(passwordMd5, credentials.getPasswordMd5()); if (!result) { throw new IncorrectCredentialsException("用户名或密码错误。"); } return true; }
认证过滤
直接在spring-shiro.xml的shiroFilter.filterChainDefinitions中定义哪些URL需要认证才能访问并不利于维护,
更合适的方法可能是写一个读取器,告诉filterChainDefinitions哪些URL不需要认证即可,需要认证的写在最后
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/" /> <property name="unauthorizedUrl" value="/html/unauthorized.html" /> <property name="filterChainDefinitions"> <value> #{filterChainDefinitionsLoader.anonUrls()} /**=authc </value> </property> </bean>
/** * Shiro的FilterChainDefinitions配置加载器 * * @author Deolin */ public class FilterChainDefinitionsLoader { @Autowired private UnauthenticationMapper unauthenticationMapper; /** * 从数据库`unauthorization`表中读取所有匿名URL * * @return FilterChainDefinitions内容片段 */ public String anonUrls() { StringBuilder sb = new StringBuilder(400); List<String> urls = authenticationMapper.listUrls(); for (String url : urls) { sb.append(url); sb.append("=anon"); sb.append(CRLF); } return sb.toString(); } }
FilterChainDefinitionsLoader.anonUrls()会在项目启动时调用,读取到所有匿名权限URL,
拼接成字符串,返回给filterChainDefinitions
授权配置
采用基于角色的权限设计,一个权限对应一个URL。一个用户间接地拥有多个权限,间接地能够访问多个权限所一一对应的URL。
这样一来,权限会全部配置在spring-shiro.xml的shiroFilter.filterChainDefinitions中,例如:
/html/students-view.html=perms["students-view"]
/student/add=perms["studentAdd"]
/student/list=perms["studentList"]
红蓝部分都是可自定义的,项目中每个涉及到权限的URL,都应该定义,
并保存到数据库`permission`表的name字段和url字段。
即`permission`是张字典表,项目中有多少个涉及到权限的URL,表中就会有多少字段。
授权配置也需要读取器
<property name="filterChainDefinitions"> <value> #{filterChainDefinitionsLoader.anonUrls()} #{filterChainDefinitionsLoader.permsUrls()} /**=authc </value> </property>
/** * 从数据库`permission`表中读取所有权限 * * @return FilterChainDefinitions内容片段 */ public String permsUrls() { StringBuilder sb = new StringBuilder(400); List<PermissionPo> pos = permissionMapper.list(); for (PermissionPo po : pos) { sb.append(po.getUrl()); sb.append("=perms[\""); sb.append(po.getName()); sb.append("\"]"); sb.append(CRLF); } return sb.toString(); }
授权流程
关键代码如下:
/** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> roleNames = accountMapper.listRoleNames(username); Set<String> permissionNames = accountMapper.listPermissionNames(username); info.setRoles(roleNames); info.setStringPermissions(permissionNames); return info; }
基于过滤器链的认证、权限检查
shiro的filterChainDefinitions是在项目启动是加载的,所以可以通过spel表达式,调用bean的方法,将各种字典表中的URL,拼接起来,返回给filterChainDefinitions,解释为“xxxxURL需要认证才能访问”“xxURL需要yyy的权限才能访问”。
这里需要事先设计好字典表。
自定义过滤器
shiro的自带过滤器,似乎无法处理AJAX请求,至少无法返回项目通用JSON数据结构,比如自带的org.apache.shiro.web.filter.authc.AuthenticationFilter类和org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter类(它们分别被过滤器链中的authc标识和perms标识所映射),它们各自的onAccessDenied方法,只用重定向处理,而没有返回JSON的处理,所以,需要专门写一些自定义过滤器,以及用于处理AJAX请求的工具类。
/** * 为shiro的自定义Filter准备的工具类<br> * <br> * 用于判断请求是否是AJAX请求和输出JSON * * @author Deolin */ public class FilterUtil { public static boolean isAJAX(ServletRequest request) { return "XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With")); } public static void out(ServletResponse response, RequestResult requestResult) { ObjectMapper jackson = new ObjectMapper(); PrintWriter out = null; try { response.setCharacterEncoding("UTF-8"); out = response.getWriter(); out.println(jackson.writeValueAsString(requestResult)); } catch (Exception e) {} finally { if (null != out) { out.flush(); out.close(); } } } }
/** * 自定义AccessControlFilter<br> * <br> * 这个类会在spring-shiro.xml注册为looseAuthc,作为filterChainDefinitions的一个自定义标记,未通过过滤的页面请求会重定向到登录页面,未通过过滤的AJAX请求会返回JSON * * @author Deolin */ public class LooseAuthcFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { Subject subject = getSubject(request, response); // “记住我”自动登录 或 输入用户名+密码手动登录 if (subject.isRemembered() || subject.isAuthenticated()) { return true; } else { return false; } } // 如果isAccessAllowed返回false,则调用这个方法 @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (FilterUtil.isAJAX(request)) { // 返回JSON对象,前端根据resp.result=-2跳转到登录页面 FilterUtil.out(response, RequestResult.unauthenticate()); } else { // 重定向到登录页面 saveRequestAndRedirectToLogin(request, response); } return false; } }
项目下载
https://github.com/spldeolin/login-demo