http協議是無狀態的,即每次發送的報文沒有任何聯繫;這就帶來一個問題:如何判斷用戶的登陸狀態?總不可能每次請求的時候都從新輸入用戶名密碼吧.因而人們使用客戶端cookie保存sessionId+服務端保存session的方式解決了這個問題.這也帶來了額外的問題:html
初始化攔截器配置,過濾登陸請求及靜態資源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; } }
Shiro是一款出色的安全框架.相較與SpringSecurity配置簡單,普遍運用於SpringBoot中.單機架構中session會交給shiro管理.web
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; }
首先須要額外引入shiro-redis插件,幫咱們實現了使用redis做爲shiro的緩存管理器.(固然你能夠不用這個依賴本身手擼)segmentfault
<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; }
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; }