关于spring:spring-AuthenticationInfo

64次阅读

共计 6369 个字符,预计需要花费 16 分钟才能阅读完成。

背景

http 协定是无状态的, 即每次发送的报文没有任何分割; 这就带来一个问题: 如何判断用户的登录状态? 总不可能每次申请的时候都从新输出用户名明码吧. 于是人们应用客户端 cookie 保留 sessionId+ 服务端保留 session 的形式解决了这个问题. 这也带来了额定的问题:

  • session 最后是存储在内存中的, 当用户量过大时, 服务器须要接受额外负担.
  • session 不利于横向拓展. 比方当你搭建集群的时候, 应用 nginx 转发申请后你并不能确定每次申请下发到了哪台服务器.

当然也衍生出了解决办法

  1. nginx 应用 ip_hash 模式. 将用户 ip hash 计算后失去一个固定值, 确保每次申请都落在同一台服务器. 当然这种做法很蠢, 因为这象征一旦某台服务器挂了, 该用户短时间将无法访问了.
  2. 将 session 保留到缓存数据库中, 如 redis.
  3. 应用 token 保留登录信息, 将 token 存储到缓存数据库如 redis.
  4. 应用 jwt 生成 token, 服务端生成 token 时会用密钥进行签名. 每次客户端申请时携带 token 能够间接验证是否是服务端签发的. 当然这种形式也存在缺点:token 的过期工夫不可批改. 这意味着一旦 token 颁发, 你无法控制其生效工夫. 某种意义上存在安全隐患. 所以有时候还是会在里面套一层 redis 管制 token 生效工夫.

demo

  • mvc 拦截器实现登录认证
  • shiro 单机环境
  • shiro-redis 集群环境
  • jwt

1.mvc 拦截器

初始化拦截器配置, 过滤登录申请及动态资源

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

 @Autowired
 private loginInteceptor loginInteceptor;
    
 @Override
 public void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/statics/**")
 .addResourceLocations("classpath:/statics/");
        registry.addResourceHandler("/*.html")
 .addResourceLocations("classpath:/templates/");
    }
   
 @Override
 public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInteceptor).excludePathPatterns("/login.html", "/statics/**"
 ,"/shiro/login");
    }
}

自定义拦截器, 若没有登录则重定向到登录页面

@Component
@Slf4j
public class loginInteceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (request.getSession().getAttribute("username") != null) {return true;}
 response.sendRedirect(request.getContextPath() + "/login.html");
        return false;
    }
}

2.Shiro 单机环境

Shiro 是一款杰出的平安框架. 相较与 SpringSecurity 配置简略, 宽泛使用于 SpringBoot 中. 单机架构中 session 会交给 shiro 治理.

Shiro 外围模块

1.subject:subject 即以后拜访零碎的用户, 能够通过 SecurityUtils 获取.
2.realm:shiro 拜访数据库校验的 dao 层.shiro 反对繁多 realm 认证和多 realm 认证.
3.SecurityManager:shiro 的外围管理器, 负责认证与受权,manager 从 relam 中获取数据库数据.
4.ShiroFilterFactoryBean:shiro 拦截器, 负责拦挡申请和放开申请, 拦挡胜利后会被申请交还给 manager 判断.

1. 登录接口
这里 session 曾经交给 shiro 治理.ShiroHttpSession 实现了 HttpSession 接口.Shiro 内置了多种异样, 这边就不展现了.

@PostMapping("login")
public Tr<?> shiroLogin(HttpSession httpSession,@RequestBody UserEntity entity) {log.info("session:{}", new Gson().toJson(httpSession));
    Subject subject = SecurityUtils.getSubject();
    try {subject.login(new UsernamePasswordToken(entity.getName(), entity.getPassword()));
        return new Tr<>(200, "登陆胜利");
    } catch (Exception e) {return new Tr<>("登录失败");
    }
}

2. 自定义 realm 作为数据交互层
重写 doGetAuthenticationInfo 进行身份验证.
留神 shiro 存储明码应用的是 char 数组, 这边须要转为 String.

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
    String password = String.valueOf(usernamePasswordToken.getPassword());
    UserEntity entity = userService.getOne(new QueryWrapper<UserEntity>()
 .eq("name", usernamePasswordToken.getUsername())
 );
    if (entity == null) {throw new UnknownAccountException("账号不存在");
    } else {if (!password.equals(entity.getPassword())) {throw new IncorrectCredentialsException("明码谬误");
        }
 } return new SimpleAccount(authenticationToken.getPrincipal(), authenticationToken.getCredentials(), getName());
}

3. 注入 shiro 管理器, 拦截器

@Bean
public CustomRealm customRealm() {return new CustomRealm();
}

/**
 * 管理器, 注入自定义的 realm
 */
@Bean("securityManager")
public SessionsSecurityManager securityManager(CustomRealm customRealm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm);
    return securityManager;
}


/**
 * shiro 过滤器,factory 注入 manager
 */
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    Map<String, String> filterMap = new LinkedHashMap<>();
// 放开拦挡
filterMap.put("/shiro/login/**","anon");
filterMap.put("login.html","anon");
// 放开动态资源
filterMap.put("/statics/**","anon");
// 拦挡所有
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
// 默认认证门路 默认 login.jsp
shiroFilterFactoryBean.setLoginUrl("/login.html");
return shiroFilterFactoryBean;
}

3.shiro-redis 集群环境

首先须要额定引入 shiro-redis 插件, 帮咱们实现了应用 redis 作为 shiro 的缓存管理器.(当然你能够不必这个依赖本人手撸)

<dependency>
 <groupId>org.crazycake</groupId>
 <artifactId>shiro-redis</artifactId>
 <version>3.1.0</version>
</dependency>

crazycake 内置的 IRedisManager 有四个实现类如图. 依据理论状况抉择一个即可

给 relam 配置缓存处理器, 当然也能够间接给 securityManager 设置. 这取决于细粒度的管制.

@Bean("customRealm")
public CustomRealm customRealm() {
 //redis
 RedisManager redisManager = new RedisManager();
    redisManager.setHost("127.0.0.1");
    redisManager.setPort(6380);
    //shiro 缓存管理器
    RedisCacheManager redisCacheManager = new RedisCacheManager();
    // 惟一标识
    redisCacheManager.setPrincipalIdFieldName("id");
    redisCacheManager.setRedisManager(redisManager);
    log.info("redis 缓存管理器:{}", new Gson().toJson(redisCacheManager));
    CustomRealm customRealm = new CustomRealm();
    // 开启全局缓存
    customRealm.setCachingEnabled(true);
    // 开启认证缓存
    customRealm.setAuthenticationCachingEnabled(true);
    customRealm.setCacheManager(redisCacheManager);
    return customRealm;
}

开启缓存后, 调用 subject 的 login 接口会优先应用缓存数据取代查 mysql.

private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
 AuthenticationInfo info = null;
    Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
    if (cache != null && token != null) {log.trace("Attempting to retrieve the AuthenticationInfo from cache.");
        Object key = getAuthenticationCacheKey(token);
        info = cache.get(key);
        if (info == null) {log.trace("No AuthorizationInfo found in cache for key [{}]", key);
        } else {log.trace("Found cached AuthorizationInfo for key [{}]", key);
        }
 }
 return info;
}

4.JWT

Json web token, 服务端依据密钥签发 token, 设置生效工夫. 客户端拜访时携带 token, 依据密钥能够直接判断是否是以后服务器签发的.jwt 的这一个性也经常用于单点登录等场景.

jwt 由 Header,Payload,Signature 组成.Header 中存储令牌类型和签名算法.Payload 存不敏感业务信息. 签名由后端依据密钥生成. 理论使用时时候会应用 base64 编码后传递.

@PostMapping("login")
public Tr<?> jwtLogin(HttpSession httpSession,@RequestBody UserEntity entity) {

UserEntity userEntity = userService.getOne(new QueryWrapper<UserEntity>().eq("name", entity.getName()));
    
 if(userEntity==null){return new Tr<>("账号不存在");}
 
 if(!entity.getPassword()
 .equals(userEntity.getPassword()))
 {return new Tr<>("明码谬误");}
 
 // 如果账号密码正确, 生成 token
 String jwtToken = JwtUtil.sign(entity.getName());
log.info("获取 token:{}",new Gson().toJson(jwtToken));
return new Tr<>(200, jwtToken,"登陆胜利");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");
log.info("获取 token:{}",token);

if(StringUtils.isNotBlank(JwtUtil.verify(token))){return true;}
 
response.sendRedirect(request.getContextPath() + "/login.html");
return false;
}

正文完
 0