blog5开发:博客等的密码访问

以前一直想弄一个关于博客的密码访问,但由于在表情站上耗费了太多的时间,一直未能成行,这里简单的实现一下。
本来打算直接在实体中添加一个password属性来作为密码判断的,这样虽然简单,但有一些不足:比如加密的方式很单一,只能通过密码判断,如果后期要做一些拓展,改动十分不便。鉴于此,稍加改进一下:
先定义一个LockResource接口,一旦对象实现了该接口,那么则表明该对象是受保护的资源,例如:

public interface LockResource {

    /**
     * 被锁保护的资源,应该提供一个唯一的ID
     * 
     * @return
     */
    String getResourceId();
    
    /**
     * 获取锁ID
     * @return
     */
    String getLockId();

}

public class Space extends Id implements LockResource {
    private String lockId;// 如果有锁,那么需要解锁

    @Override
    public String getResourceId() {
        return "Space-" + alias;
    }

    @Override
    public String getLockId() {
        return lockId;
    }

}

然后定义一个锁对象Lock

public abstract class Lock {

    private String id;
    private String name;
    private User user;
    private LockResource lockResource;

    /**
     * 从请求中获取钥匙
     * 
     * @param request
     */
    public abstract LockKey getKeyFromRequest(HttpServletRequest request);

    /**
     * 开锁
     */
    public abstract void tryOpen(LockKey key) throws LockException;

    /**
     * 解锁地址
     * 
     * @return
     * @throws LockException
     */
    public abstract String keyInputUrl() throws LockException;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LockResource getLockResource() {
        return lockResource;
    }

    public void setLockResource(LockResource lockResource) {
        this.lockResource = lockResource;
    }

}

最后是用来解锁的LockKey:

public interface LockKey {
    
    Object getKey();

}

LockException用于解锁失败:

public class LockException extends RuntimeException {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    private Lock lock;

    public Lock getLock() {
        return lock;
    }

    public LockException(Lock lock) {
        this.lock = lock;
    }

}

基本逻辑如下:匿名访问受保护的资源(这里为space),根据space的lockId获取该对象的保护锁,然后通过用户持有的LockKey判断是否能解锁,如果能够解锁,返回资源,否则直接抛出LockException异常,前端捕获该异常,获取异常中的锁对象,暂存锁对象和资源ID,将抛出该异常的地址作为redirectUrl参数,重定向到锁的解锁地址,用户在解锁地址输入用于解锁的内容之后,后台取出暂存的锁,用于解锁(这个锁是暂存的,不是实时的),解锁成功后重定向到redirectUrl,这就回到了匿名访问受保护的资源这一步(这里会取实时的锁用于判断,防止期间锁的解锁方式发生了改变)。

简单的实现:
用户访问链接:www.abc.com:8080/space/life/test,进入action,查询别名为life的space:

  @ModelAttribute
    protected final Space putSpaceInModel(@PathVariable("alias") String alias) throws SpaceNotFoundException {
        Space space = userService.selectSpaceByAlias(alias);
        if (space == null) {
            throw new SpaceNotFoundException(alias);
        }
        return space;
    }

userService.selectSpaceByAlias如下:

@Override
    @Cacheable(value = "userCache", key = "'space-'+#alias")
    @LockProtected
    public Space selectSpaceByAlias(String alias) {
        Space space = spaceDao.selectByAlias(alias);
        if (space != null) {
            switch (space.getStatus()) {
            case DISABLE:
                return null;
            default:
                return space;
            }
        }
        return null;
    }


该方法拥有一个LockProtected annotation,LockAspect用于处理该annotation:

@Aspect
@Order(1)
public class LockAspect {

    @Autowired
    private LockManager<?> lockManager;

    @AfterReturning(value = "@within(LockProtected) || @annotation(LockProtected)", returning = "lockResource")
    public void after(LockResource lockResource) throws Throwable {
        // 需要验证密码
        if (lockResource != null && lockResource.getLockId() != null && UserContext.get() == null) {
            Lock lock = lockManager.findLock(lockResource.getLockId());
            if (lock != null) {
                lock.setLockResource(lockResource);
                LockKey key = LockKeyContext.getKey(lockResource.getResourceId());
                lock.tryOpen(key);
            }
        }
    }
}
 拥有该annotation的方法必须返回lockResource对象
 如果抛出异常后依旧仍需要被Cacheable缓存,那么Order应该比CacheInterceptor 优先级高,
 CacheInterceptor优先级默认为Ordered.LOWEST_PRECEDENCE
  
至此,完成了资源的保护判断,接下来捕获验证失败后的 LockException异常:
 @ResponseStatus(HttpStatus.FORBIDDEN) // 403
    @ExceptionHandler(LockException.class)
    public String handleLockException(HttpServletRequest request, LockException ex) throws IOException {
        HttpSession session = request.getSession();
        String resourceId = (String) session.getAttribute(Constants.LAST_RESOURCE_SESSION_KEY);
        if (resourceId != null) {
            //防止session中Lock过多
            session.removeAttribute(resourceId);
        }
        Lock lock = ex.getLock();
        String url = lock.keyInputUrl();
        if (!UrlUtils.isAbsoluteUrl(url)) {
            url = urlStragey.getUrl(request) + (url.startsWith("/") ? url : "/" + url);
        }
        String redirectUrl = UrlUtils.buildFullRequestUrl(request);
        if (urlStragey.getSpaceIfSpaceDomainRequest(request) != null) {
            redirectUrl = UrlUtils.getUrlBeforeForward(request);
        }
        UriComponents uc = UriComponentsBuilder.fromHttpUrl(url).queryParam("redirectUrl", redirectUrl).build();
        session.setAttribute(Constants.LAST_RESOURCE_SESSION_KEY, lock.getLockResource().getResourceId());
        session.setAttribute(lock.getLockResource().getResourceId(), lock);
        return "redirect:" + uc.toUriString();
    }

被重定向至了如下页面:
1475114367766174546.PNG

用户点击确定提交输入的问题,用于解锁:
@SuppressWarnings("unchecked")
    @RequestMapping(value = "unlock", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult unlock(@RequestParam("redirectUrl") String redirectUrl, HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return new JsonResult(false, "请求失效");
        }
        String resourceId = (String) session.getAttribute(Constants.LAST_RESOURCE_SESSION_KEY);
        if (resourceId == null) {
            return new JsonResult(false, "请求失效");
        }
        Lock lock = (Lock) session.getAttribute(resourceId);
        if (lock == null) {
            return new JsonResult(false, "请求失效");
        }
        LockKey key = null;
        try {
            key = lock.getKeyFromRequest(request);
            lock.tryOpen(key);
        } catch (LockException e) {
            return new JsonResult(false, "解锁失败,请重新解锁");
        }
        Map<String, LockKey> keysMap = (Map<String, LockKey>) session.getAttribute(Constants.LOCKKEY_SESSION_KEY);
        if (keysMap == null) {
            keysMap = new HashMap<String, LockKey>();
        }
        keysMap.put(resourceId, key);
        session.setAttribute(Constants.LOCKKEY_SESSION_KEY, keysMap);
        session.removeAttribute(resourceId);
        session.removeAttribute(Constants.LAST_RESOURCE_SESSION_KEY);
        return new JsonResult(true, "解锁成功");
    }
LockKeyContext是一个上下文,将session中的Map<String,LockKey>取出后放入 ThreadLocal中,用于验证。
public class LockKeyContext {

    private static final ThreadLocal<Map<String, LockKey>> keysLocal = new ThreadLocal<Map<String, LockKey>>();

    public static LockKey getKey(String id) {
        Map<String, LockKey> keyMap = keysLocal.get();
        return keyMap == null ? null : keyMap.get(id);
    }

    public static void remove() {
        keysLocal.remove();
    }

    public static void set(Map<String, LockKey> keysMap) {
        keysLocal.set(keysMap);
    }

}