Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
SecurityManager即安全管理器
,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
Authenticator即认证器
,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer即授权器
,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm即领域
,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
sessionManager即会话管理
,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO即会话dao
,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
CacheManager即缓存管理
,将用户权限数据存储在缓存,这样可以提高性能。
Cryptography即密码管理
,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
<!--JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--shiro-redis依赖-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
第一步:创建shiro过滤器,让shiro管理项目资源
/**
* 第一步:创建Shiro过滤器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition){
//创建shiro过滤器
//要让shiro管理web应用,首先需要让shiro接管资源,所以创建一个过滤器
ShiroFilterFactoryBean shiroFilterFactoryBean= new ShiroFilterFactoryBean();
//给拦截器设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//这是自定义拦截器的一些函数方法
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", new JwtFilter());//设置自定义的拦截器
shiroFilterFactoryBean.setFilters(filters);
/**
* Map map = new LinkedHashMap<>();
* map.put("/**" , "authc");
* shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
* 下面的方法作用和上面的一样,只不过专门使用一个shiroFilterChainDefinition()封装起来
* shiroFilterChainDefinition()里面的设置拦截器的拦截路径
**/
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
第二步:创建上面使用的安全管理器
/**
* 第二步:创建安全管理器
* 设置session和cache
* @param accountRealm
* @param sessionManager
* @param redisCacheManager
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
SessionManager sessionManager,
RedisCacheManager redisCacheManager) {
//创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
//设置自定义Realm
securityManager.setRealm(accountRealm);
//设置自定义session管理
securityManager.setSessionManager(sessionManager);
// 设置redis的cache
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
第三步:创建第一步使用到shiroFilterChainDefinition,设置哪些可以不用认证就可以访问,哪些资源受限
/**
* 配置公共资源和受保护资源
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
//退出登录
filterMap.put("/logout", "logout");
// swagger
filterMap.put("/swagger**/**", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/v2/**", "anon");
filterMap.put("/**", "jwt"); // 使用自定义的拦截器进行拦截处理
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
第四步:设置第二步使用到的sessionManager
/**
* 自定义session,设置redisSessionDAO
* @param redisSessionDAO
* @return
*/
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 注入redisSessionDAO,缓存为Redis则需改用Redis的管理
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
/**
*如果原来的请求头有token,就返回JwtToken中,在executeLogin()方法使用createToken
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取 token
HttpServletRequest request = (HttpServletRequest) servletRequest;
//获取头部信息
String jwt = request.getHeader("token");
if(StringUtils.isEmpty(jwt)){
return null;
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取 token
HttpServletRequest request = (HttpServletRequest) servletRequest;
//获取头部信息
String token = request.getHeader("token");
if(StrUtil.isEmpty(token)){
//不进行拦截
return true;
}else {
// 效验token,判断是否已过期
System.out.println(token);
//通过token拿到claim,效验claim
Claims claim = jwtUtils.getClaimByToken(token);
if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
throw new ExpiredCredentialsException("token已失效,请重新登录!");
}
}
// 执行自动登录,最后委托自定义Realm执行认证
return executeLogin(servletRequest, servletResponse);
}
//登录失败,返回错误信息
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
//返回错误的信息
JsonResult jsonResult = new JsonResult(ResponseStatusEnum.ERROR.getCode(),throwable.getMessage());
//转换为json
String json = JSONUtil.toJsonStr(jsonResult);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
return false;
}
//解决跨域的问题
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(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"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
SysAdminService sysAdminService;
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//强制类型转换
JwtToken jwtToken = (JwtToken) token;
//没有token,认证失败
if (jwtToken.getPrincipal() == null) {
return null;
}
//获取账号
String adminAccount = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
QueryWrapper<SysAdmin> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("admin_account",adminAccount);
SysAdmin admin = sysAdminService.getOne(queryWrapper);
if (admin==null){
throw new UnknownAccountException("账户不存在");
}
if (admin.getAdminEnabled()==0){
throw new UnknownAccountException("账户已被禁用");
}
//设置返回的信息
AccountProfile profile = new AccountProfile();
BeanUtil.copyProperties(admin,profile);
return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName());
}
}
/**
1. jwt工具类
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "manage.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 获取token中的负载信息
* @param token
* @return
*/
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
/**
* AuthenticationToken的实现类
* 具体实现过程在这里面
*/
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt) {
this.token = jwt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
首先,所有请求交给ShiroFilter的过滤器,由过滤器进行管理。shiroFilterFactoryBean设定安全管理器securityManager和自定义过滤器JwtFilter,安全管理器securityManager设定自定义的Realm。
再者,自定义一个过滤器对请求进行拦截,对于没有token进行放行,对于有token的,效验token是否正确和是否失效。如果都正确就执行executeLogin(),调用Realm的**doGetAuthenticationInfo()**对其进行认证。