很多公司app客户端和服务端通讯是基于socket自定义私有协议,但对于创业型公司和需要快速搭建app原型的公司,http通讯是比较好的选择,开发效率高,http协议会比socket协议通信慢,且容易被刷协议和截取数据包。那么基于http协议的通信,登录和鉴权是比较重要的环节。最近在研究springside4,shiro,基于这些框架搭建了一套app http登录和鉴权。
下图先介绍整个逻辑流程:
登录服务端代码:
@Controller
@RequestMapping(value = "/loginuser")
public classUserLogin {
@Autowired
protected AccountServiceaccountService;
@Autowired
private CacheManagerehcacheManager;
/**
* 设定安全的密码,生成随机的salt并经过1024次sha-1 hash
*/
private StringentryptPassword(String salt,String plainPassWord) {
byte[] decodeSalt=Encodes.decodeHex(salt);
byte[] hashPassword =Digests.sha1(plainPassWord.getBytes(), decodeSalt, Constants.HASH_INTERATIONS);
return Encodes.encodeHex(hashPassword);
}
//@RequestMapping(value = "/loginUser", method= RequestMethod.POST)
@RequestMapping(value = "/{username}/{password}", method = RequestMethod.GET)
@ResponseBody
public String log(@PathVariable("username")String userName,@PathVariable("password") String passWord) {
//根据用户名查找数据库
Useruser = accountService.findUserByLoginName(userName);
if (user !=null) {
if(user.getStatus().equals("disabled")) {
throw newDisabledAccountException();
}
//数据库存的密码为加密后的,需要把客户端传过来的密码明文进行加密
StringdescPassWord=entryptPassword(user.getSalt(),passWord);
if(user.getPassword().equals(descPassWord))
{
//根据KEY,用户ID,当前时间戳生成token
StringclientToken=HmacSHA256Utils.digest(Constants.KEY, String.valueOf(user.getId()+System.currentTimeMillis()));
//存放在ehcache,可以改为存放在redis
Elementelement = newElement(user.getId(), clientToken);
Cachecache=ehcacheManager.getCache(Constants.LOGIN_TOKEN_CACHE_NAME);
cache.put(element);
//返回token给客户端
return clientToken;
}
else
return null;
}
else
return null;
}
}
public class Constants {
public static final String PARAM_DIGEST = "digest";
public static final String PARAM_USERNAME = "username";
public static final String PARAM_ID = "id";
public static final String LOGIN_TOKEN_CACHE_NAME = "logintoken";
//摘要,用来加密生成token
public static String KEY="qwertyasdfoimdfdk";
public static final String HASH_ALGORITHM = "SHA-1";
public static final int HASH_INTERATIONS = 1024;
}
服务端鉴权使用filter组装数据后委托shiro 的realm处理
public classStatelessAuthcFilterextendsAccessControlFilter {
@Autowired
private CacheManagerehcacheManager;
@Override
protected boolean isAccessAllowed(ServletRequestrequest, ServletResponse response, Object mappedValue)throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequestrequest, ServletResponse response)throws Exception {
//客户端的url的数字签名
String clientDigest =request.getParameter(Constants.PARAM_DIGEST);
//2、客户端传入的用户身份
String username =request.getParameter(Constants.PARAM_USERNAME);
String id =request.getParameter(Constants.PARAM_ID);
HttpServletRequesthttpRequest=(HttpServletRequest) request;
//删除掉URL的数字签名
String digestParm="digest="+clientDigest;
String url=httpRequest.getRequestURL()+"?"+httpRequest.getQueryString().replace("&"+digestParm,"").replace(digestParm,"");
//4、生成无状态Token
StatelessToken token = new StatelessToken(Long.parseLong(id),username,url, clientDigest);
try {
//5、委托给Realm进行登录
getSubject(request,response).login(token);
} catch (Exception e) {
e.printStackTrace();
onLoginFail(response); //6、登录失败
return false;
}
return true;
}
//登录失败时默认返回401状态码
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse =(HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}
public classStatelessRealm extends AuthorizingRealm {
protected AccountServiceaccountService;
@Autowired
private CacheManagerehcacheManager;
@Override
publicbooleansupports(AuthenticationToken token) {
//仅支持StatelessToken类型的Token
return tokeninstanceof StatelessToken;
}
@Override
protectedAuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//根据用户名查找角色,请根据需求实现
SimpleAuthorizationInfoauthorizationInfo = newSimpleAuthorizationInfo();
authorizationInfo.addRole("admin");
return authorizationInfo;
}
@Override
protectedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException{
StatelessToken statelessToken =(StatelessToken) token;
String username =statelessToken.getUsername();//用户名
long id=statelessToken.getId();//用户ID
Cache cache=ehcacheManager.getCache(Constants.LOGIN_TOKEN_CACHE_NAME);
Element tokenElement=cache.get(id);
String strtoken=(String)tokenElement.getObjectValue();
//在服务器端生成客户端参数消息摘要
String url=statelessToken.getUrl();
String serverDigest = HmacSHA256Utils.digest(strtoken,url);
System.out.println("ClientDigest:"+statelessToken.getClientDigest());
System.out.println("serverDigest"+serverDigest);
//然后进行客户端消息摘要和服务器端消息摘要的匹配
return new SimpleAuthenticationInfo(
username,
serverDigest,
getName());
}
}
客户端测试代码如下
/**
* Hello world!
*
*/
public classApp
{
static RestTemplaterestTemplate =new RestTemplate();
publicstaticfinalStringPARAM_DIGEST="digest";
publicstaticfinalStringPARAM_USERNAME="username";
public static String userName="admin";
public static String passWord="admin";
public static String userId="1";
publicstaticvoidmain( String[] args )
{
//登录获取token
String token =login();
MultiValueMap
params.add(PARAM_USERNAME,userName);
params.add("id", userId);
//构造请求URL
String url = UriComponentsBuilder
.fromHttpUrl("http://localhost:8080/showcase/api/v1/user/1.json")
.queryParams(params).build().toUriString();
//对URL进行签名
String digest=HmacSHA256Utils.digest(token,url);
//在URL上带上签名
params.add(PARAM_DIGEST, digest);
url= UriComponentsBuilder
.fromHttpUrl("http://localhost:8080/showcase/api/v1/user/1.json")
.queryParams(params).build().toUriString();
System.out.println("url:"+url);
ResponseEntity
System.out.println("responseEntity:"+responseEntity.getBody());
}
/**
* 登录返回token
* @return
*/
publicstaticString login()
{
String loginUrl="http://localhost:8080/showcase/loginuser/"+userName+"/"+passWord;
ResponseEntity
return (String)responseEntity.getBody();
}
}