spring AuthenticationInfo

背景

http協議是無狀態的,即每次發送的報文沒有任何聯繫;這就帶來一個問題:如何判斷用戶的登陸狀態?總不可能每次請求的時候都從新輸入用戶名密碼吧.因而人們使用客戶端cookie保存sessionId+服務端保存session的方式解決了這個問題.這也帶來了額外的問題:html

  • 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攔截器

初始化攔截器配置,過濾登陸請求及靜態資源mysql

@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");
    }
}

自定義攔截器,若沒有登陸則重定向到登陸頁面nginx

@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管理.web

Shiro核心模塊

1.subject:subject即當前訪問系統的用戶,能夠經過SecurityUtils獲取.
2.realm:shiro訪問數據庫校驗的dao層.shiro支持單一realm認證和多realm認證.
3.SecurityManager:shiro的核心管理器,負責認證與受權,manager從relam中獲取數據庫數據.
4.ShiroFilterFactoryBean:shiro攔截器,負責攔截請求和放開請求,攔截成功後會被請求交還給manager判斷.redis

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.sql

@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的緩存管理器.(固然你能夠不用這個依賴本身手擼)segmentfault

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

crazycake內置的IRedisManager有四個實現類如圖.根據實際狀況選擇一個便可
image.png後端

給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;
}
相關文章
相關標籤/搜索