它是一个功能强大、灵活的,优秀开源的安全框架。html
它能够处理身份验证、受权、企业会话管理和加密。前端
它易于使用和理解,相比Spring Security入门门槛低。java
Shiro 的总体框架以下图所示:git
Authentication(认证), Authorization(受权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。web
它们分别是:redis
除此以外,还有其余的功能来支持和增强这些不一样应用环境下安全领域的关注点。特别是对如下的功能支持:算法
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager 和 Realm。下面的图展现了这些组件如何相互做用,咱们将在下面依次对其进行描述。spring
咱们须要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是受权访问控制,用于对用户进行的操做受权,证实该用户是否容许进行当前操做,如访问某个连接,某个资源文件等。数据库
以上描述摘抄自纯洁的微笑博客文章,更多详情能够参考:apache
Shiro 官网:http://shiro.apache.org/
纯洁的微笑:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html
下面就来说解如何在咱们的项目里集成 Shiro 框架。
首先上 maven 仓库查找,当前最新的版本是 1.4.0,咱们就用这个版本。
kitty-pom/pom.xml 父POM中添加属性和 dependencyManagement 依赖
<shiro.version>1.4.0</shiro.version>
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency>
kitty-admin/pom.xml 添加 dependencies 依赖
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> </dependency>
同理,把后续要用到的几个工具包也导入进来。
<!-- fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency>
<!-- commons -->
<dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>${commons.lang.version}</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>${commons.fileupload.version}</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons.io.version}</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>${commons.codec.version}</version> </dependency>
添加配置类,注入自定义的认证过滤器(OAuth2Filter)和认证器(OAuth2Realm),并添加请求路径拦截配置。
ShiroConfig.java
package com.louis.kitty.boot.config; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.Filter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.louis.kitty.admin.oauth2.OAuth2Filter; import com.louis.kitty.admin.oauth2.OAuth2Realm; /** * Shiro 配置 * @author Louis * @date Sep 1, 2018 */ @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); // 自定义 OAuth2Filter 过滤器,替代默认的过滤器 Map<String, Filter> filters = new HashMap<>(); filters.put("oauth2", new OAuth2Filter()); shiroFilter.setFilters(filters); // 访问路径拦截配置,"anon"表示无需验证,未登陆也可访问 Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/webjars/**", "anon"); // 查看SQL监控(druid) filterMap.put("/druid/**", "anon"); // 首页和登陆页面 filterMap.put("/", "anon"); filterMap.put("/sys/login", "anon"); // swagger filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resources", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/webjars/springfox-swagger-ui/**", "anon"); // 其余全部路径交给OAuth2Filter处理 filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean public Realm getShiroRealm(){ OAuth2Realm myShiroRealm = new OAuth2Realm(); return myShiroRealm; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注入 Realm 实现类,实现本身的登陆逻辑 securityManager.setRealm(getShiroRealm()); return securityManager; } }
拦截除配置成不需认证的请求路径外的请求,都交由这个过滤器处理,负责接收前台带过来的token并封装成对象,若是请求没有携带token,则提示错误。
OAuth2Filter.java
package com.louis.kitty.admin.oauth2; import java.io.IOException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import com.alibaba.fastjson.JSONObject; import com.louis.kitty.common.utils.StringUtils; import com.louis.kitty.core.http.HttpResult; import com.louis.kitty.core.http.HttpStatus; /** * Oauth2过滤器 * @author Louis * @date Sep 1, 2018 */ public class OAuth2Filter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { // 获取请求token String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isBlank(token)){ return null; } return new OAuth2Token(token); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 获取请求token,若是token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isBlank(token)){ HttpServletResponse httpResponse = (HttpServletResponse) response; HttpResult result = HttpResult.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"); String json = JSONObject.toJSONString(result); httpResponse.getWriter().print(json); return false; } return executeLogin(request, response); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json; charset=utf-8"); try { // 处理登陆失败的异常 Throwable throwable = e.getCause() == null ? e : e.getCause(); HttpResult result = HttpResult.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage()); String json = JSONObject.toJSONString(result); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * 获取请求的token */ private String getRequestToken(HttpServletRequest httpRequest){ // 从header中获取token String token = httpRequest.getHeader("token"); // 若是header中不存在token,则从参数中获取token if(StringUtils.isBlank(token)){ token = httpRequest.getParameter("token"); } return token; } }
OAuth2Token.java
package com.louis.kitty.admin.oauth2; import org.apache.shiro.authc.AuthenticationToken; /** * 自定义 token 对象 * @author Louis * @date Sep 1, 2018 */ public class OAuth2Token implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token; public OAuth2Token(String token){ this.token = token; } @Override public String getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
逻辑认证器是认证和受权的主体逻辑,主要包含两部分。
doGetAuthenticationInfo:实现本身的登陆验证逻辑,这里主要是认证 token。
doGetAuthorizationInfo:实现接口受权逻辑,收集权限标识或角色,用来断定接口是否能够访问
OAuth2Realm.java
package com.louis.kitty.admin.oauth2; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.louis.kitty.admin.model.SysUser; import com.louis.kitty.admin.model.SysUserToken; import com.louis.kitty.admin.sevice.SysUserService; import com.louis.kitty.admin.sevice.SysUserTokenService; /** * 认证Realm实现 * @author Louis * @date Sep 1, 2018 */ @Component public class OAuth2Realm extends AuthorizingRealm { @Autowired SysUserService sysUserService; @Autowired SysUserTokenService sysUserTokenService; @Override public boolean supports(AuthenticationToken token) { return token instanceof OAuth2Token; } /** * 受权(接口保护,验证接口调用权限时调用) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SysUser user = (SysUser)principals.getPrimaryPrincipal(); // 用户权限列表,根据用户拥有的权限标识与如 @permission标注的接口对比,决定是否能够调用接口 Set<String> permsSet = sysUserService.findPermissions(user.getUsername()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(permsSet); return info; } /** * 认证(登陆时调用) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getPrincipal(); // 根据accessToken,查询用户token信息 SysUserToken sysUserToken = sysUserTokenService.findByToken(token); if(sysUserToken == null || sysUserToken.getExpireTime().getTime() < System.currentTimeMillis()){ // token已经失效 throw new IncorrectCredentialsException("token失效,请从新登陆"); } // 查询用户信息 SysUser user = sysUserService.findById(sysUserToken.getUserId()); // 帐号被锁定 if(user.getStatus() == 0){ throw new LockedAccountException("帐号已被锁定,请联系管理员"); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, token, getName()); return info; } }
完善登陆逻辑,在用户密码匹配成功以后,建立并保存token,最后将token返回给前台,之后请求带上token。
SysLoginController.java
/** * 登陆接口 */ @PostMapping(value = "/sys/login") public HttpResult login(@RequestBody LoginBean loginBean) throws IOException { String username = loginBean.getUsername(); String password = loginBean.getPassword(); // 用户信息 SysUser user = sysUserService.findByUserName(username); // 帐号不存在、密码错误 if (user == null) { return HttpResult.error("帐号不存在"); } if (!match(user, password)) { return HttpResult.error("密码不正确"); } // 帐号锁定 if (user.getStatus() == 0) { return HttpResult.error("帐号已被锁定,请联系管理员"); } // 生成token,并保存到数据库 SysUserToken data = sysUserTokenService.createToken(user.getUserId()); return HttpResult.ok(data); } /** * 验证用户密码 * @param user * @param password * @return */ public boolean match(SysUser user, String password) { return user.getPassword().equals(PasswordUtils.encrypte(password, user.getSalt())); }
SysUserTokenServiceImpl.java,生成并保存token,这里把token保存在数据库,也能够选择保存在redis或session。
@Override public SysUserToken createToken(long userId) { // 生成一个token String token = TokenGenerator.generateToken(); // 当前时间 Date now = new Date(); // 过时时间 Date expireTime = new Date(now.getTime() + EXPIRE * 1000); // 判断是否生成过token SysUserToken sysUserToken = findByUserId(userId); if(sysUserToken == null){ sysUserToken = new SysUserToken(); sysUserToken.setUserId(userId); sysUserToken.setToken(token); sysUserToken.setLastUpdateTime(now); sysUserToken.setExpireTime(expireTime); // 保存token,这里选择保存到数据库,也能够放到Redis或Session之类可存储的地方 save(sysUserToken); } else{ sysUserToken.setToken(token); sysUserToken.setLastUpdateTime(now); sysUserToken.setExpireTime(expireTime); // 若是token已经生成,则更新token的过时时间 update(sysUserToken); } return sysUserToken; }
登陆 Swagger: localhost:8088/swagger-ui.html
用户名:admin 密码: admin
登陆成功以后,会返回token,以下图所示。
登陆成功以后,通常的逻辑是调到主页,这里咱们能够继续访问一个接口看成登陆成功以后的跳转(如 /dept/findTree,不用传参方便)。
而后咱们就会发现调用失败,甚至打断点到目标接口代码,链接口代码都没有进来,根本没有调用到findTree接口。
这是必然的,由于引入乐Shiro以后便有了权限认证,若是访问请求没有携带token是不能经过验证的,具体解决方案参加下面的登陆流程。
为了帮助你们理解 shiro 的工做流程,这里对使用了 shiro 之后,咱们项目的登陆流程作一下简单的说明。
咱们开启Debug模式,给登陆接口及过滤器和认证器都打上断点,调用登陆接口,跟着代码移动的脚步来了解整个登陆的流程。
首先代码来到了咱们调用的接口: login
成功验证用户密码,即将生成和保存token
根据条件生成或更新token,成功后登陆接口会将token返回给前台,前台会带上token进入登陆验证
登陆接口返回以后就已经登陆成功了,按照通常逻辑,这时就会跳转到主页了,咱们这边没有页面,就经过访问接口来模拟吧。
咱们访问Swagger里 dept/findTree 接口,获取机构数据,这个接口不用传参,比较方便。
结果发现访问没有访问正常结果,甚至debug发现连对应的后台接口代码都没有进去。那是由于加了shiro之后,访问除配置放过外的接口都是须要验证的。
咱们直接在浏览器访问:http://localhost:8088/dept/findTree,发现代码来到了咱们在过滤器设置的断点里边。
由于咱们访问接口的时候,没有把刚才登陆成功以后返回的token信息携带过来,因此在过滤器里验证token失败,返回"invalid token" 提示
果真,在代码执行完毕以后,页面获得 “invalid token” 的提示,那咱们要继续访问还得带上token才行。
那怎样才能让 swagger 发送请求的时候把 token 也带过去呢,咱们这样处理。
修改 Swagger 配置,添加请求头参数,用来传递 token。
SwaggerConfig.java
package com.louis.kitty.boot.config; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ParameterBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.schema.ModelRef; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Parameter; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi(){ // 添加请求参数,咱们这里把token做为请求头部参数传入后端 ParameterBuilder parameterBuilder = new ParameterBuilder(); List<Parameter> parameters = new ArrayList<Parameter>(); parameterBuilder.name("token").description("令牌") .modelRef(new ModelRef("string")).parameterType("header").required(false).build(); parameters.add(parameterBuilder.build()); return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()) .build().globalOperationParameters(parameters); // return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) // .select() // .apis(RequestHandlerSelectors.any()) // .paths(PathSelectors.any()).build(); } private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("Kitty API Doc") .description("This is a restful api document of Kitty.") .version("1.0") .build(); } }
重启代码,发现接口页面已经多了token请求参数了。
咱们先调用登陆接口,拿到返回的token以后,把token复制过来一块儿发送过去。
继续用 amdin 用户登陆,得到返回 token
携带 token 再次访问 findTree 接口。
代码进入过滤器,发现 token 已经成功传过来了,往下执行 executeLogin 继续登陆流程。
上面方法调用下面的接口,尝试从请求头或请求参数中获取token。
父类的 executeLogin 方法调用 createToken 建立 token,而后使用 Subject 进行登陆。
过滤器的 createToken 方法返回咱们自定义的 token 对象。
Subject 调用 SecurityManager 继续进行登陆流程。
看下面的调用栈截图,通过系列操做以后,终于来到了咱们的 OAuth2Realm,这里有咱们的登陆和受权逻辑。
来到 OAuth2Realm 的 doGetAuthenticationInfo 方法,将前台传递的token跟后台存储的作比对,比对成功继续往下走。
验证成功以后,代码终于来到了咱们的目标接口,成功的完成了调用。
继续往前,放行代码,代码执行完毕,调用界面成功的返回告终果。
咱们不传 token 或者传一个不存在的 token 试试。
发现代码在过滤器验证的时候没有经过,返回 “Token 失效” 提示。
接口响应结果,提示 “token失效,请从新登陆”。
最后注意:加了Shiro以后每次调试接口都须要传递token,对咱们开发来讲也是麻烦,若有须要能够经过如下方法取消验证。
在 ShiroConfig 配置类中,把接口路径映射到 anon 过滤器,调试时就不须要 token 验证了。
后端:https://gitee.com/liuge1988/kitty
前端:https://gitee.com/liuge1988/kitty-ui.git
做者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/ 版权全部,欢迎转载,转载请注明原文做者及出处。