Apache Shiro是一个轻量级的安全框架
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。 Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。其基本功能点如下图所示:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
- Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
接下来我们分别从外部和内部来看看Shiro的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的API, 且API契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject。
- Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的 *** 作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
- Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行 *** 作;可以把Realm看成DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个Shiro应用:
- 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
- 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。
接下来我们来从Shiro内部来看下Shiro的架构,如下图所示:
- Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
- SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的 *** 作;即控制着用户能访问应用中的哪些功能;
- Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
- SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
- SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
- Maven依赖pom.xml
4.0.0 org.example springboot-shrio-jwt1.0-SNAPSHOT spring-boot-parent org.springframework.boot 2.1.3.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-webcom.baomidou mybatis-plus-boot-starter3.1.2 mysql mysql-connector-javaruntime org.springframework.boot spring-boot-devtoolscompile true org.projectlombok lombok1.18.4 com.alibaba druid-spring-boot-starter1.1.17 com.alibaba fastjson1.2.58 org.apache.shiro shiro-spring-boot-starter1.4.1 com.auth0 java-jwt3.7.0 org.springframework.boot spring-boot-starter-data-redisorg.apache.commons commons-pool2org.apache.maven.plugins maven-compiler-plugin1.8
- 核心配置类
package com.kongliand.shiro.config; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan(value = {"com.kongliand.shiro.mapper"}) public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } @Bean public PerformanceInterceptor performanceInterceptor() { return new PerformanceInterceptor(); } }
package com.kongliand.shiro.config; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.*; import javax.annotation.Resource; import java.lang.reflect.Method; import java.time.Duration; import java.util.Arrays; import static java.util.Collections.singletonMap; @Configuration @EnableCaching // 开启缓存支持 public class RedisConfig extends CachingConfigurerSupport { @Resource private LettuceConnectionFactory lettuceConnectionFactory; @Override @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getDeclaringClass().getName()); Arrays.stream(params).map(Object::toString).forEach(sb::append); return sb.toString(); } }; } @Bean public RedisTemplateredisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { // 设置序列化 Jackson2JsonRedisSerializer
package com.kongliand.shiro.config; import com.kongliand.shiro.filter.JwtFilter; import com.kongliand.shiro.shiro.ShiroRealm; import org.apache.shiro.mgt.DefaultSessionStorageevaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import javax.servlet.Filter; import java.util.HashMap; import java.util.linkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 拦截器 MapfilterChainDefinitionMap = new linkedHashMap (); // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除 filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除 filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("*.js", "anon"); filterChainDefinitionMap.put("*.css", "anon"); filterChainDefinitionMap.put("*.html", "anon"); filterChainDefinitionMap.put("*.jpg", "anon"); filterChainDefinitionMap.put("*.png", "anon"); filterChainDefinitionMap.put("*.ico", "anon"); filterChainDefinitionMap.put("/druid DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageevaluator defaultSessionStorageevaluator = new DefaultSessionStorageevaluator(); defaultSessionStorageevaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageevaluator(defaultSessionStorageevaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
3.鉴权登录拦截器
package com.kongliand.shiro.filter; import com.kongliand.shiro.constant.CommonConstant; import com.kongliand.shiro.entity.JwtToken; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { throw new AuthenticationException(e.getMessage()); } } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN); JwtToken jwtToken = new JwtToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 getSubject(request, response).login(jwtToken); // 如果没有抛出异常则代表登入成功,返回true return true; } @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETe"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
4.用户登录鉴权和获取用户授权
package com.kongliand.shiro.shiro; import com.kongliand.shiro.constant.CommonConstant; import com.kongliand.shiro.entity.JwtToken; import com.kongliand.shiro.entity.SysUser; import com.kongliand.shiro.service.ISysUserService; import com.kongliand.shiro.util.CommonUtils; import com.kongliand.shiro.util.JwtUtil; import com.kongliand.shiro.util.RedisUtil; import com.kongliand.shiro.util.SpringContextUtils; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; 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.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import java.util.Set; @Component @Slf4j public class ShiroRealm extends AuthorizingRealm { @Autowired @Lazy private ISysUserService sysUserService; @Autowired @Lazy private RedisUtil redisUtil; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("————权限认证 [ roles、permissions]————"); SysUser sysUser = null; String username = null; if (principals != null) { sysUser = (SysUser) principals.getPrimaryPrincipal(); username = sysUser.getUserName(); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 设置用户拥有的角色集合,比如“admin,test” SetroleSet = sysUserService.getUserRolesSet(username); info.setRoles(roleSet); // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add” Set permissionSet = sysUserService.getUserPermissionsSet(username); info.addStringPermissions(permissionSet); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); if (token == null) { log.info("————————身份认证失败——————————IP地址: " + CommonUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest())); throw new AuthenticationException("token为空!"); } // 校验token有效性 SysUser loginUser = this.checkUserTokenIsEffect(token); return new SimpleAuthenticationInfo(loginUser, token, getName()); } public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException { // 解密获得username,用于和数据库进行对比 String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法无效!"); } // 查询用户信息 SysUser loginUser = new SysUser(); SysUser sysUser = sysUserService.getUserByName(username); if (sysUser == null) { throw new AuthenticationException("用户不存在!"); } // 校验token是否超时失效 & 或者账号密码是否错误 if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) { throw new AuthenticationException("Token失效请重新登录!"); } // 判断用户状态 if (!"0".equals(sysUser.getDelFlag())) { throw new AuthenticationException("账号已被删除,请联系管理员!"); } BeanUtils.copyProperties(sysUser, loginUser); return loginUser; } public boolean jwtTokenRefresh(String token, String userName, String passWord) { String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token)); if (CommonUtils.isNotEmpty(cacheToken)) { // 校验token有效性 if (!JwtUtil.verify(cacheToken, userName, passWord)) { String newAuthorization = JwtUtil.sign(userName, passWord); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization); // 设置超时时间 redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000); } else { redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken); // 设置超时时间 redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000); } return true; } return false; } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wdbq8hpS-1639299147506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
5.application.yml配置信息
server: port: 8088 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver druid: url: jdbc:mysql://127.0.0.1:3306/jwt?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: admin123 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 #validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true #redis配置 redis: database: 0 host: 127.0.0.1 lettuce: pool: max-active: 8 #最大连接数据库连接数,设 0 为没有限制 max-idle: 8 #最大等待连接中的数量,设 0 为没有限制 max-wait: -1ms #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 min-idle: 0 #最小等待连接中的数量,设 0 为没有限制 shutdown-timeout: 100ms password: '' port: 6379 #mybatis plus设置 mybatis-plus: type-aliases-package: com.kongliand.shiro.entity mapper-locations: classpath:mapper/*.xml global-config: banner: false db-config: #主键类型 id-type: auto # 默认数据库表下划线命名 table-underline: true configuration: map-underscore-to-camel-case: true # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志配置 logging: level: com.kongliand.shiro.mapper: debug
最后开始验证一下
1.获取token
2.根据token请求接口
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)