在网络时代中,每一家网站都应重视其用户的登陆体验及安全性问题。当用户返回本网站时,所携带的看似简单的code码实则包含了用户的sessionID,代表了用户的身份及其状态。本文将详细剖析此过程中所存在的安全风险,并提出相应的设计策略以确保用户的安全与便捷性。
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=授权后跳转的uri
示例:
https://api.weibo.com/oauth2/authorize?
client_id=刚才申请的APP-KEY &
response_type=code&
redirect_uri=http://gulimall.com/success
一、sessionID的意义与重要性
https://api.weibo.com/oauth2/access_token?
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
grant_type=authorization_code&
redirect_uri=YOUR_REGISTERED_REDIRECT_URI&
code=CODE
在用户访问站点之际,系统会生成唯一的sessionID,作为用户的身份标识。凭借着这一特点,系统得以识别和记忆真实用户,提供个性化服务。然而,sessionID的隐私保护非常关键,若被恶意用户窃取,将有可能引发用户数据泄露及账户失窃等问题。
{
"access_token": "SlAV32hkKG",
"remind_in": 3600, # 也是声明周期,但将废弃
"expires_in": 3600 # access_token的生命周期;
}
实际上,对SESSIONID的管理非比寻常。必须保证每一次用户接入,都会产生一个全新的、独特的SESSIONID,并在用户离开后立即失效。同时,对于SESSIONID的存储,要格外小心,防止其存放在不安全的位置,以防在传输过程中被窃取。唯有如此,方能有效保障用户的隐私与安全。
@GetMapping("/weibo/success") // Oath2Controller
public String weiBo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 根据code换取 Access Token
Map<String,String> map = new HashMap<>();
map.put("client_id", "1294828100");
map.put("client_secret", "a8e8900e15fba6077591cdfa3105af44");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
Map<String, String> headers = new HashMap<>();
// 去获取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);
if(response.getStatusLine().getStatusCode() == 200){
// 获取响应体: Access Token
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
// 相当于我们知道了当前是那个用户
// 1.如果用户是第一次进来 自动注册进来(为当前社交用户生成一个会员信息 以后这个账户就会关联这个账号)
R login = memberFeignService.login(socialUser);
if(login.getCode() == 0){
MemberRsepVo rsepVo = login.getData("data" ,new TypeReference<MemberRsepVo>() {});
log.info("\n欢迎 [" + rsepVo.getUsername() + "] 使用社交账号登录");
// 第一次使用session 命令浏览器保存这个用户信息 JESSIONSEID 每次只要访问这个网站就会带上这个cookie
// 在发卡的时候扩大session作用域 (指定域名为父域名)
// TODO 1.默认发的当前域的session (需要解决子域session共享问题)
// TODO 2.使用JSON的方式序列化到redis
// new Cookie("JSESSIONID","").setDomain("gulimall.com");
session.setAttribute(AuthServerConstant.LOGIN_USER, rsepVo);
// 登录成功 跳回首页
return "redirect:http://gulimall.com";
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}
二、API调用与用户数据的获取
@RequestMapping("/oauth2/login")
public R login(@RequestBody SocialUser socialUser) {
MemberEntity entity=memberService.login(socialUser);
if (entity!=null){
return R.ok().put("memberEntity",entity);
}else {
return R.error();
}
}
@Override // 已经用code生成了token
public MemberEntity login(SocialUser socialUser) {
// 微博的uid
String uid = socialUser.getUid();
// 1.判断社交用户登录过系统
MemberDao dao = this.baseMapper;
MemberEntity entity = dao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
MemberEntity memberEntity = new MemberEntity();
if(entity != null){ // 注册过
// 说明这个用户注册过, 修改它的资料
// 更新令牌
memberEntity.setId(entity.getId());
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
// 更新
dao.updateById(memberEntity);
entity.setAccessToken(socialUser.getAccessToken());
entity.setExpiresIn(socialUser.getExpiresIn());
entity.setPassword(null);
return entity;
}else{ // 没有注册过
// 2. 没有查到当前社交用户对应的记录 我们就需要注册一个
HashMap<String, String> map = new HashMap<>();
map.put("access_token", socialUser.getAccessToken());
map.put("uid", socialUser.getUid());
try {
// 3. 查询当前社交用户账号信息(昵称、性别、头像等)
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), map);
if(response.getStatusLine().getStatusCode() == 200){
// 查询成功
String json = EntityUtils.toString(response.getEntity());
// 这个JSON对象什么样的数据都可以直接获取
JSONObject jsonObject = JSON.parseObject(json);
memberEntity.setNickname(jsonObject.getString("name"));
memberEntity.setUsername(jsonObject.getString("name"));
memberEntity.setGender("m".equals(jsonObject.getString("gender"))?1:0);
memberEntity.setCity(jsonObject.getString("location"));
memberEntity.setJob("自媒体");
memberEntity.setEmail(jsonObject.getString("email"));
}
} catch (Exception e) {
log.warn("社交登录时远程调用出错 [尝试修复]");
}
memberEntity.setStatus(0);
memberEntity.setCreateTime(new Date());
memberEntity.setBirth(new Date());
memberEntity.setLevelId(1L);
memberEntity.setSocialUid(socialUser.getUid());
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
// 注册 -- 登录成功
dao.insert(memberEntity);
memberEntity.setPassword(null);
return memberEntity;
}
}
在用户成功登录后,系统将生成唯一的AccessToken,借助此令牌,可调用API获取用户头像及昵称等详细信息。尽管流程看似简易,实则蕴含着严谨的验证机制。每次API调用均需对用户身份进行核实,以确保仅授权用户方可访问其个人信息。
为了确保持续性的同时加强网络的安全性,当今的加密技术可用于对Token进行加密与签名,以确保其在传输过程中的安全。另外,定期更新Token的有效期并及时失效过期的Token,是防范恶意攻击的关键措施。通过这些策略,用户的数据将得到充分的保护。
三、拦截器与安全访问控制
访问首页需验证,Shiro守卫来保障。访问策略权重分,身份明晰方通行。
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
拦截器作为一种高效工具,既可提高系统安全性,又能改善用户体验。对普通用户而言,我们设计了简易便捷的登陆界面;对高级权限用户如管理员等,则实施更为严谨的身份认证机制。如此一来,便能有效防范未经授权的访问,确保系统的安全无忧。
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=192.168.56.10
四、跨系统的用户认证
@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
public class GulimallAuthServerApplication {
当前网络环境下,用户频繁在各类系统间穿梭,因而单点登录(SSO)技术应运而生。当用户成功注册并登录至SSO系统后,即可在各子系统间自由切换,极大地提高了使用便利性。然而,这一设计也引发了全新的安全隐患。
@Configuration
public class GulimallSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSIONID"); // cookie的键
serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
例如,SSO系统的用户在成功登录后,必须通过各个子系统检验所生成的ServiceTicket(ST)。若此环节处理不妥当,可能造成安全隐患,使恶意者有机可乘伪造身份。因此,保证ST的有效性与安全性至关重要,系统需在每次访问时对其进行核实,以确保仅有合法用户得以授权。
@Import({RedisHttpSessionConfiguration.class})
@Configuration( proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
五、ThreadLocal与本地数据共享
public class RedisHttpSessionConfiguration
extends SpringHttpSessionConfiguration // 继承
implements 。。。{
// 后面SessionRepositoryFilter会构造时候自动注入他
@Bean // 操作session的方法,如getSession() deleteById()
public RedisIndexedSessionRepository sessionRepository() {
为提升系统性能,我们采用ThreadLocal以实现用户数据共享。凭借此方式,用户信息可储存在本地内存,从而避免频繁访问远程会话,减轻系统负荷。然而,需谨慎运用,确保数据安全。
public class SpringHttpSessionConfiguration
implements ApplicationContextAware {
@Bean
public SessionRepositoryFilter extends Session> springSessionRepositoryFilter(SessionRepository sessionRepository) { // 注入前面的bean
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
尽管ThreadLocal有助于提高效率,然而若管理不当,可能引发诸如内存泄露等问题。因此,必须在用户退出环节妥善清除ThreadLocal内存储的数据,以防潜在的安全风险。此外,应精心设计数据共享策略,使系统能够高效运作且保障用户数据安全。
@Override // SessionRepositoryFilter.java
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//对原生的request、response进行包装
// SessionRepositoryRequestWrapper.getSession()
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
@GetMapping({"/login.html","/","/index","/index.html"}) // auth
public String loginPage(HttpSession session){
// 从会话从获取loginUser
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);// "loginUser";
System.out.println("attribute:"+attribute);
if(attribute == null){
return "login";
}
System.out.println("已登陆过,重定向到首页");
return "redirect:http://gulimall.com";
}
@PostMapping("/login") // auth
public String login(UserLoginVo userLoginVo,
RedirectAttributes redirectAttributes,
HttpSession session){
// 远程登录
R r = memberFeignService.login(userLoginVo);
if(r.getCode() == 0){
// 登录成功
MemberRespVo respVo = r.getData("data", new TypeReference<MemberRespVo>() {});
// 放入session // key为loginUser
session.setAttribute(AuthServerConstant.LOGIN_USER, respVo);//loginUser
log.info("\n欢迎 [" + respVo.getUsername() + "] 登录");
// 登录成功重定向到首页
return "redirect:http://gulimall.com";
}else {
HashMap<String, String> error = new HashMap<>();
// 获取错误信息
error.put("msg", r.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors", error);
return "redirect:http://auth.gulimall.com/login.html";
}
}