spring AuthenticationInfo

背景

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()
 .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 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的缓存管理器.(当然你可以不用这个依赖自己手撸)


 org.crazycake
 shiro-redis
 3.1.0

crazycake内置的IRedisManager有四个实现类如图.根据实际情况选择一个即可
spring AuthenticationInfo_第1张图片

给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 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().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;
}

你可能感兴趣的:(springshirojwt)