基于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

 

posted @ 2017-10-08 16:18  Deolin  阅读(1384)  评论(0编辑  收藏  举报