背景

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拦截器

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

@Configurationpublic 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@Slf4jpublic 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.

@Overrideprotected 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管理器,拦截器

@Beanpublic 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.jspshiroFilterFactoryBean.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,"登陆胜利");}
@Overridepublic 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;}