项目背景:该项目用来为app项目提供数据,在项目中只返回接口数据,不返回视图,并禁用了session,自定义了过滤器和token。
项目中使用的技术:springboot+maven+shiro。需要先了解一哈。
首先对shiro框架做个说明:
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。其中有3个核心组件,Subject, SecurityManager 和 Realms。
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
Realm是整个框架中为数不多的必须由设计者自行实现的模块。
先新建一个springboot项目,创建后配置好maven,项目结构如下:
其中的service,mapper等是用来整合mysql,本次项目就简暂不使用。
项目pom.xml文件引用jar包配置如下
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- log4j --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.2</version> </dependency> <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency> <!-- google --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> </dependencies>
在shiro文件夹下新建JWTToken
/** * 自定义shiro的token */ public class JWTToken implements AuthenticationToken { /** * 密钥 */ private String token; public JWTToken(String token){ this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
新建MyShiroRealm,定义权限,在app上,就如首页是无需用户登录的,即首页是用户登录不登录都是可以访问的,但是用户的个人信息就需要登录才能访问。
/** * 自定义Realm <领域> */ public class MyShiroRealm extends AuthorizingRealm{ /** * 使用自定义token */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } /** * 权限配置 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); System.out.println("判断用户是否有权限访问此连接"); String url = request.getRequestURI(); String method = request.getMethod(); System.out.println(url+"------------"+method); //设计的验证权限的逻辑内容 return authorizationInfo; } /** * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 获取用户的token String authorization = String.valueOf(token.getPrincipal()); //这里验证token和simpleAuthenticationInfo的信息 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(authorization, authorization, getName());
//暂定密码为111,应该根据信息查询用户并判断用户的身份 if (!authorization.equals("111")){ throw new AuthenticationException("密码错误!"); } return authenticationInfo; } }
新建StatelessDefaultSubjectFactory
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { // 不创建session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
新建JWTFilter,过滤器。
/** * 自定义jwt过滤器 */ public class JWTFilter extends AccessControlFilter { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); //过滤器过滤用户是否有权限 @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpRequest = (HttpServletRequest) request; LOGGER.info("当前用户访问url:" + httpRequest.getRequestURL().toString()); //根据Header取出相关信息,之后放入Token String authorization = httpRequest.getHeader("authorization"); try { if (StringUtils.isEmpty(authorization)) { throw new AuthenticationException("请求错误!"); } // 获取无状态Token JWTToken jwtToken = new JWTToken(authorization); // 委托给Realm进行登录,会跳转doGetAuthenticationInfo方法进行判断用户 getSubject(request, response).login(jwtToken); boolean isContinue = true;
//这里对需要用户登录但试无需访问权限的公共url进行了判断 if (httpRequest.getRequestURI().indexOf("index") >= 0){ isContinue = false; } /************ 公用的配置跳过权限验证 end ****************/ if (isContinue) { // 通过isPermitted 才能调用doGetAuthorizationInfo方法获取权限信息 getSubject(request, response).isPermitted(httpRequest.getRequestURI()); } } catch (AuthenticationException e) { response401(httpRequest, response, e.getMessage()); return false; } return true; } //在访问被拒绝运行 @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { System.out.println("请求被拒绝"); return false; } /** * 将非法请求跳转到 /401 */ private void response401(ServletRequest req, ServletResponse resp, String message) { try { req.setAttribute("message", message); req.getRequestDispatcher("/401").forward(req, resp); } catch (IOException ex) { LOGGER.error(ex.getMessage()); } catch (ServletException e1) { LOGGER.error(e1.getMessage()); } return; } }
新建GlobalExceptionResolver
/** * 异常处理 */ public class GlobalExceptionResolver implements HandlerExceptionResolver { private static Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ModelAndView mv; // 进行异常判断。如果捕获异常请求跳转。 if (ex instanceof UnauthorizedException) { mv = new ModelAndView("/user/unauth"); return mv; } else { mv = new ModelAndView(); FastJsonJsonView view = new FastJsonJsonView(); // ex.printStackTrace(); logger.error("shiro错误!!", ex); Map<String, Object> map = new HashMap<>(); String beanString = JSON.toJSONString("服务器异常"); map = JSON.parseObject(beanString, Map.class); view.setAttributesMap(map); mv.setView(view); return mv; } } }
新建ShiroConfig
@Configuration public class ShiroConfig { private final static Logger LOGGER = LoggerFactory.getLogger(ShiroConfig.class); //将自己的验证方式加入容器 @Bean(name = "customRealm") public MyShiroRealm customRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } //权限管理,配置主要是Realm的管理认证 @Bean(name = "securityManager") public DefaultWebSecurityManager defaultWebSecurityManager(MyShiroRealm customRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); StatelessDefaultSubjectFactory statelessDefaultSubjectFactory = new StatelessDefaultSubjectFactory(); securityManager.setSubjectFactory(statelessDefaultSubjectFactory); /* * 关闭shiro自带的session,详情见文档 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); //将自定义的realm让其管理使用 securityManager.setRealm(customRealm); securityManager.setSessionManager(defaultSessionManager()); return securityManager; } /** * 会话管理类 禁用session */ @Bean public DefaultSessionManager defaultSessionManager() { LOGGER.info("=============ShiroConfig.getDefaultSessionManager()======"); DefaultSessionManager manager = new DefaultSessionManager(); manager.setSessionValidationSchedulerEnabled(false); return manager; } //Filter工厂,设置对应的过滤条件和跳转条件 @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { LOGGER.info("===========init shiroFilterFactory============"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 添加自己的过滤器并且取名为jwt Map<String, Filter> filterMap = Maps.newHashMap(); filterMap.put("jwtFilter", new JWTFilter()); shiroFilterFactoryBean.setFilters(filterMap); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = Maps.newLinkedHashMap(); // 注意过滤器配置顺序 不能颠倒 // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl filterChainDefinitionMap.put("/401", "anon"); // 配置不会被拦截的链接 顺序判断 // filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/logout", "anon"); filterChainDefinitionMap.put("/login", "anon");
//除放行的url外,其他的url都让JWTFilter进行拦截过滤 filterChainDefinitionMap.put("/**","jwtFilter"); // 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 // shiroFilterFactoryBean.setLoginUrl("/logout"); //错误页面,认证不通过跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/401"); // 登录成功后要跳转的链接 // shiroFilterFactoryBean.setSuccessUrl("/index"); // 未授权界面 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } //加入注解的使用,不加入这个注解不生效 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 注册全局异常处理 * * @return */ @Bean(name = "exceptionHandler") public HandlerExceptionResolver handlerExceptionResolver() { return new GlobalExceptionResolver(); } }
新建UserController
@RestController @RequestMapping("/user") public class UserController{ @GetMapping("/insert") public String insert(@RequestParam(value = "username", required = false)String username,@RequestParam(value = "password", required = false)String password){ System.out.println(username+":"+password); return "ok"; } }
启动项目,进行访问
header中需要传anthorization参数并赋值为111,我们在isAccessAllowed方法中获取header并在doGetAuthenticationInfo方法中进行了密码效验。
控制台输出
可以在shiroFilterFactoryBean方法中进行放行,不再拦截过滤。
再次访问,输出为
可以看到没有对当前url进行拦截了。