看了一个可扩展用户登录的设计,确实想的比我要全面。看似登录一个小的功能,要考虑的东西还是很多的,安全性上,可扩展性,代码的风格上,作者就是从这些角度来考虑,具体可以看看下面参考资料中的设计,考虑了本地账户登录,OAuth2协议登录等。我看过之后按照设计实现了下,当然大体上只实现了本地用户的登录,第三方登录的就没做了。总的来说,设计思想还是很受启发的。当然你可以先看参考资料中的思想,回过头来再看实现就更简单了。
本地数据库设计
create table bp_user
(
`user_id` int auto_increment comment '登录ID',
`state` int comment '状态',
`emp_id` int comment '员工ID',
`update_user` int comment '更新人',
`update_time` datetime comment '更新时间',
`delete_flag` int comment '删除标志',
primary key (user_id)
)AUTO_INCREMENT=1000;
alter table bp_user comment '用户';
create table bp_auth_local
(
`user_id` int comment '登录ID',
`username` varchar(64) not null comment '姓名',
`password` varchar(32) not null comment '密码',
`update_user` int comment '更新人',
`update_time` datetime comment '更新时间',
`delete_flag` int comment '删除标志',
primary key (user_id)
);
alter table bp_auth_local comment '用户本地验证';
注:如果是第三方登录OAuth,那我们类似的根据实际情况增加第二张表就可以了,如果区分QQ微信,微博,可以增加一个字段来区分。再或者其他方式的登录,只需要相应的增加第二张表即可,达到登录方式存储的扩展,并且使得用户信息与验证信息单独分开存储,避免信息的泄露。
验证接口及实现
为了能兼容多种登录方式,首先我们需要为所有的验证方式定义一个接口Authenticator
:
public interface Authenticator {
LoginUser authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticateException;
}
现在要实现本地用户登录,那我们需要只需要实现这个接口LocalCookieAuthenticator
:
public class LocalCookieAuthenticator implements Authenticator {
@Autowired
IAuthLocalService authLocalService;
/**
* 验证
*/
public LoginUser authenticate(HttpServletRequest request, HttpServletResponse response) {
String reqCookie = CookieHelper.getCookieByName(request, AuthConstant.LOGIN_USER_COOKIE_KEY);
if (reqCookie == null) {
return null;
}
ServletContext sc = request.getSession().getServletContext();
XmlWebApplicationContext cxt = (XmlWebApplicationContext)WebApplicationContextUtils.getWebApplicationContext(sc);
if(cxt != null && cxt.getBean("authLocalService") != null && authLocalService == null){
authLocalService = (IAuthLocalService) cxt.getBean("authLocalService");
}
LoginUser user= getUserByCookie(reqCookie);
return user;
}
/**
* 检查cookie
* @param tocken
* @return
*/
private LoginUser getUserByCookie(String tocken){
if(!StringUtils.isEmpty(tocken)){
return authLocalService.checkTockenValid(tocken);
}
return null;
}
}
当然,Basic认证的Authorization Header,我们需要一个BasicAuthenticator
,对于用API Token认证的方式,同样编写一个APIAuthenticator
。这样做,每增加一种验证方式,我们仅需增加类似上面的相应的具体实现类即可
实际上这里有一个验证Cookie的地方,就要考虑到安全性,既不能泄露泄露作为注册账号的邮箱等信息,也不能被轻易的破解或者伪造。那如何验证以及生成Cookie,我们将实现放到后面在来做。
过滤器Filter实现
接下来我们需要实现一个Filter来过滤所有的请求,AuthenticationFilter
实现如下:
public class AuthenticationFilter implements Filter {
// 忽略的过滤地址
private String excludePages;
private String[] excludePagesArray;
// 所有的Authenticator都在这里
Authenticator[] authenticators = initAuthenticators();
// 每个页面都会执行
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 链式认证获得User:
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
LoginUser user=null ;
if(!excludeRequest(httpRequest)){
user = tryGetAuthenticatedUser(httpRequest, httpResponse);
if(user==null){
try {
String url=httpRequest.getRequestURI();
//判断获取的路径不为空且不是访问登录页面或执行登录操作时跳转
if(!StringUtils.isEmpty(url) && ( url.indexOf("Login")<0 && url.indexOf("login")<0 )) {
httpResponse.sendRedirect(httpRequest.getContextPath() + "/login.jsp");
return;
}
}
catch(Exception e){
}
}else{
}
}
// 把User绑定到UserContext中:
try (UserContext ctx = new UserContext(user)) {
chain.doFilter(request, response);
}catch(Exception e){
}
}
private LoginUser tryGetAuthenticatedUser(HttpServletRequest request, HttpServletResponse response){
LoginUser user = null;
try{
for (Authenticator auth : this.authenticators) {
user = auth.authenticate(request,response);
if (user != null) {
break;
}
}
}catch(Exception e){}
return user;
}
/**
* 忽略的请求
* @param request
* @return
*/
private boolean excludeRequest(HttpServletRequest request){
for (String pageUrl : excludePagesArray) {//判断是否在过滤url之外
if(request.getServletPath().equals(pageUrl)){
return true;
}
}
return false;
}
/**
* 创建登录方式
* @return
*/
private Authenticator[] initAuthenticators(){
LocalCookieAuthenticator local = new LocalCookieAuthenticator();
return new Authenticator[]{local};
}
/**
* 初始化Filter配置
*/
public void init(FilterConfig filterConfig) throws ServletException {
excludePages = filterConfig.getInitParameter("excludePages");
if (StringUtils.isNotEmpty(excludePages)) {
excludePagesArray = excludePages.split(",");
}
return;
}
public void destroy() {
// TODO Auto-generated method stub
}
}
上面我只创建了本地用户验证的方式,如果我们创建了多种用户验证,则串联来验证,直到某一个验证通过为止。
绑定用户
那验证成功后的user
对象我们放在了UserContext
里,UserContext的实现如下:
public class UserContext implements AutoCloseable {
static final ThreadLocal<LoginUser> current = new ThreadLocal<LoginUser>();
public UserContext(LoginUser user) {
current.set(user);
}
public static LoginUser getCurrentUser() {
return current.get();
}
public void close() {
current.remove();
}
}
配置过滤器Filter
Filter实现了以后,接下来我们还需要将AuthenticationFilter
配置到web.xml中以使过滤器生效。
<!-- 登录验证过滤器 -->
<filter>
<filter-name>authenticationFilter</filter-name>
<filter-class>com.wte.configuration.filter.AuthenticationFilter</filter-class>
<init-param>
<param-name>excludePages</param-name>
<param-value>/story/bp/user/login.do</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>authenticationFilter</filter-name>
<url-pattern>/story/*</url-pattern>
</filter-mapping>
好了,这里我们基本上已经实现了验证的基本框架。总结下流程:首先Filter会过滤所有的请求,除去忽略的请求之外,对我们创建的验证方式顺序执行,直到验证通过为止,通过验证的User放到UserContext
中,在我们业务代码里可以直接getCurrentUser()
来获取登录用户。
Tocken验证及生成的实现
到这里我们好像还落下了上面提到的Cookie,来看看Cookie的生成的实现:
@Service
public class AuthLocalService extends AbstractService implements IAuthLocalService {
@Resource
protected AuthLocalDao authLocalDao;
private String cookieExpireTime = PropertyUtils.getProperty(PropertyConstant.COOKIE_EXPIRE_TIME);
/**
* 登录
*/
public OperationResult login(AuthLocalBean localAuthBean, HttpServletRequest request,
HttpServletResponse response) {
if (!StringUtils.isEmpty(localAuthBean.getUsername()) && !StringUtils.isEmpty(localAuthBean.getPassword())) {
String encryptedPassword = HashHelper.MD5Encode(localAuthBean.getPassword(), null);
AuthLocalBean entity = authLocalDao.findBy(localAuthBean.getUsername(), encryptedPassword);
if (entity != null) {
int minute = 20;
if (!StringUtils.isEmpty(cookieExpireTime)) {
Integer expireMinute = Integer.valueOf(cookieExpireTime);
if (expireMinute != null) {
minute = expireMinute.intValue();
}
}
long expireTime = this.generateCookieExpireTime(minute);
String tocken = this.generateCookieValue(entity, expireTime);
Cookie cookie = new Cookie(AuthConstant.LOGIN_USER_COOKIE_KEY, tocken);
cookie.setMaxAge(minute * 60);
cookie.setPath("/");
response.addCookie(cookie);
} else {
return new OperationResult(OperationResultType.Error);
}
}
return new OperationResult(OperationResultType.Success);
}
public OperationResult logout(HttpServletRequest request, HttpServletResponse response) {
Cookie cookie = new Cookie(AuthConstant.LOGIN_USER_COOKIE_KEY, "");
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
return new OperationResult(OperationResultType.Success);
}
/**
* 验证Tocken
*
* @param tocken
* @return
*/
public LoginUser checkTockenValid(String tocken) {
String[] tockenParams = tocken.split(":");
if (tockenParams.length == 4) {
String validTime = tockenParams[2];
if (checkValidDate(validTime)) {
return this.checkTockenValid(tocken, tockenParams);
}
}
return null;
}
/**
* 验证Tocken
*/
private LoginUser checkTockenValid(String tocken, String[] tockenParams) {
AuthLocalBean entity = authLocalDao.find(tockenParams[0]);
if (entity != null) {
String tempTocken = this.generateCookieValue(entity, tockenParams[2], tockenParams[3]);
if (tocken.equals(tempTocken)) {
return new LoginUser(entity.getUserId(), entity.getUsername());
}
}
return null;
}
/**
* 验证Tocken有效期
* @param strTime
* @return
*/
private boolean checkValidDate(String strTime){
Long time= Long.valueOf(strTime);
return DateUtils.getCurrentDate().getTime()< time;
}
/**
* 生成cookie有效时间
*
* @param expireTime
* 有效小时数
* @return
*/
private long generateCookieExpireTime(int expireTime) {
Date date = DateUtils.getCurrentDate();
Calendar ca = Calendar.getInstance();
ca.setTime(date);
ca.add(Calendar.MINUTE, expireTime);
return ca.getTime().getTime();
}
/**
* 生成tocken
*
* @param user
* @param expireTime
* @return
*/
private String generateCookieValue(AuthLocalBean user, long expireTime) {
int randomNum = (int) (Math.random() * (9999 - 1000 + 1)) + 1000;
return this.generateCookieValue(user, String.valueOf(expireTime), String.valueOf(randomNum));
}
/**
* 生成tocken
*
* @param userName
* @param encryptedPassword
* @param expireTime
* @return
*/
private String generateCookieValue(AuthLocalBean user, String expireTime, String randomNum) {
String tempOriginStr = user.getUsername() + ":" + user.getPassword() + ":" + expireTime + ":" + randomNum;
String reEncryptedPassword = HashHelper.MD5Encode(tempOriginStr, null);
String cookieValue = user.getUserId() + ":" + reEncryptedPassword + ":" + expireTime + ":" + randomNum;
return cookieValue;
}
}
可以看generateCookieValue()
里面,密码我们生成的步骤如下:
1. 组合字符串 user_id:password:expireTime:randomNum
,设置变量名为tempOriginStr
,其中password本身是数据库中经过HashHelper.MD5Encode()
处理存储的。user_id使用无意义的值使得用户信息不会被利用。
2. 对tempOriginStr进行加密处理。
3. 重新进行组合再次生成字符串 user_id:tempOriginStr:expireTime:randomNum
,作为我们响应的Cookie的Value值。
再来看看checkTockenValid()
方法验证Cookie:
1. 对Cookie的Vlaue进行Split处理(),根据第三位即expireTime与当前时间来检查Tocken有效期;
2. 用第一位即user_id来查找用户信息。
3. 根据用户信息,调用generateCookieValue()
,以及CookieValue的第三位即expireTime以及第四位randomNum来生成tocken,
4. 用生成的tocken与CookieValue的tocken进行比对,如果吻合则验证成功。否则失败。
现在我们的整个登录的验证就已经实现了,那至于验证中HashHelper.MD5Encode()的具体实现你可以随意的写了。其他的验证方式,我们也只是参考上面的代码相应增加验证的具体实现就可以了。如果有不明白的,可以去看参考资料,这里实现的思想都是从参考资料中的思想而来。