基于shiro的单点登录Java实现
关于单点登录,这里不再赘述,简单地说就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。实现单点登录的思想有很多,本篇就简单实现其中一种单点登录.
如图,用户通过sso系统登录,进行shiro身份认证, 这里做了简化处理,并没有和数据库进行交互,而是选择直接放行:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//身份认证方法:AuthRealm.doGetAuthenticationInfo()
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
Map<String, Object> map = new HashMap<>();
map.put("username", token.getUsername());
map.put("password", token.getPassword());
// 模拟从数据库获取对应用户名密码的用户
User user = new User(token.getUsername(),String.valueOf(token.getPassword()));
/*if (null == user) {
throw new AccountException("帐号或密码不正确!");
}*/
SecurityUtils.getSubject().getSession().setAttribute(UserConfig.LOGIN_USER,user);
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
|
当用户成功登陆后,会进行一次重定向,同时生成sid,这里的sid采用shiro的md5加密,密码作为盐值,生成用户名的散列,但是存在一个问题,相同认证用户的sid相同,导致黑客从中获取sid的话,以后都可以将其作为身份标识进行单点登录,因此我们可以使用RedisTemplate的set方法:
set void set(K key, V value, long timeout, TimeUnit unit);
使用:redisTemplate.opsForValue().set("name","tom",10, TimeUnit.SECONDS);
结果:redisTemplate.opsForValue().get("name")由于设置的是10秒失效,十秒之内查询有结果,十秒之后返回为null
|
可以看出来,这个方法可以将一个值设置到redisTemplate对象,并且可以设置失效时间完成缓存,这样我们每次都可以以当前时间作为盐值,随机生成md5散列,这样当失效时间一过,sid便无法使用
@RequestMapping("/main")
public String mainPage(Model model,HttpSession session){
User user = (User) session.getAttribute(UserConfig.LOGIN_USER);
String sid = Encrypt.md5(user.getUsername(),user.getPassword());
model.addAttribute("sid",sid);
return "main";
}
|
用户认证成功重定向到”/main”api,然后在返回的model中携带sid,返回main.html
<body>
<h1>主页欢迎你</h1>
<a th:href = "@{'http://localhost:8801/?callback=http://localhost:8800/sso&sid='+${sid}}">子系统1</a>
<a th:href = "@{'http://localhost:8802/?callback=http://localhost:8800/sso&sid='+${sid}}">子系统2</a>
</body>
|
main.html动态生成了子系统的访问地址,并携带两个参数:callback回调地址和sid,当用户点击a标签之后,重定向到子系统
@RequestMapping
public String home(@RequestParam("callback") String callback, @RequestParam("sid") String sid, RedirectAttributes attr) throws Exception {
System.out.println(callback);
System.out.println(sid);
//参数直接加载url后面
URL url=new URL(callback+"?sid="+sid);
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
String jsonString = "";
if(conn.getResponseCode()==200){ //200表示请求成功
InputStream is=conn.getInputStream(); //以输入流的形式返回
//将输入流转换成字符串
ByteArrayOutputStream baos=new ByteArrayOutputStream();
byte [] buffer=new byte[1024];
int len=0;
while((len=is.read(buffer))!=-1){
baos.write(buffer, 0, len);
}
jsonString=baos.toString();
baos.close();
is.close();
System.out.println(jsonString);
//转换成json数据处理
}
User user = JSON.parseObject(jsonString, User.class);
attr.addFlashAttribute("msg","欢迎用户"+user.getUsername());
return "redirect:/index";
}
|
如上,点击a标签之后进入home方法,此时拿到回调地址可以使用HttpClient或者URLconnection访问回调接口.
@RequestMapping("/sso") @ResponseBody public User sso(@RequestParam("sid") String sid,HttpSession session) throws Exception{ FileInputStream in = new FileInputStream(UserConfig.FILE_LOCATION); byte[] arr = new byte[4096]; in.read(arr); in.close(); String str = new String(arr); str = str.trim(); System.out.println(str); User user = JSON.parseObject(str, User.class); if(user ==null){ return null; } String mySid = Encrypt.md5(user.getUsername(), user.getPassword()); if(!sid.equals(mySid)){ return null; } return user; }
如上,这里是sso系统的”/sso”接口,回调请求发出后,此接口接收响应,并从根据sid进行匹配,从数据库中取到登录用户的身份信息(这里简化,使用的是本地文件保存),然后将用户的json数据传输回去.此时子系统就可以得到登录用户的身份信息,实现自动登录.最终效果:
如果不是通过单点登录跳转子系统,则无法得到登录用户信息:
文章出自上海传智JavaEE就业51期(20170422面授) 万晓鹏