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; }