引入相关的依赖
<!--shiro-redis-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
编写配置
ShiroConfig
import com.demo.shiro.AccountReaIm;
import com.demo.shiro.LoginReaIm;
import com.demo.shiro.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.crazycake.shiro.RedisSessionDAO;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/***
* 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
*/
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
/**
* 重建了SessionManager
*
* @param redisSessionDAO
* @return
*/
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
/**
* 重建DefaultWebSecurityManager
* DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,
* 这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
*
* @param accountRealm
* @param sessionManager
* @param redisCacheManager
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(LoginReaIm loginReaIm,
AccountReaIm accountRealm,
SessionManager sessionManager,
RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
/**
* 多个LoginReaIm
*/
ArrayList<Realm> arrayList=new ArrayList<>();
arrayList.add(accountRealm);
arrayList.add(loginReaIm);
securityManager.setRealms(arrayList);
return securityManager;
}
/**
* 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,
* 而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。
* 跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。
*
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
//拦截器
Map<String, String> filterMap = new LinkedHashMap<>();
// filterMap.put("/test/**", "anon");
配置不会被拦截的链接 顺序判断
filterMap.put("/**", "jwt");
definition.addPathDefinitions(filterMap);
return definition;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt",jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
AccountRealm 验证JWT
AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是
@Component
public class AccountReaIm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
TestService service;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authenticationToken;
String userid = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
Test test=service.selectByPrimaryKey( Integer.parseInt(userid));
if (test == null) {
throw new UnknownAccountException("账户不存在");
}
login profile = new login();
BeanUtil.copyProperties(test, profile);
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
}
LoginReaIm 验证登录
与上面的区别验证的Token不一样
@Log4j2
@Component
public class LoginReaIm extends AuthorizingRealm {
@Autowired
TestService service;
@Autowired
JwtUtils jwtUtils;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof
UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
*shiro 身份验证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userId = token.getPrincipal().toString();
Test test=service.selectByPrimaryKey( Integer.parseInt(userId));
if (test == null) {
throw new UnknownAccountException("账户不存在");
}
login profile = new login();
BeanUtil.copyProperties(test, profile);
return new SimpleAuthenticationInfo(profile,test.getName(), getName());
}
}
JwtToken
/**
* 我们需要重写 AuthenticationToken接口 此接口的作用
* AuthenticationToken: shiro中负责把username,password生成用于验证的token的封装类
* 自定义一个对象用来封装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;
}
}
JwtFilter
import cn.hutool.json.JSONUtil;
import com.demo.exception.AjaxResponse;
import com.demo.exception.CustomExceptionType;
import com.demo.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;
import java.io.IOException;
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return null;
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return true;
} else {
//校验Jwt
Claims claims = jwtUtils.getClaimByToken(jwt);
if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
throw new ExpiredCredentialsException("token已失效,请重新登录");
}
//执行登录
return executeLogin(servletRequest, servletResponse);
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
AjaxResponse result = AjaxResponse.error(CustomExceptionType.USER_INPUT_ERROR, throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
httpServletResponse.getWriter().print(json);
} catch (IOException ioException) {
}
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);
}
}
JwtUtils是个生成和校验jwt的工具类
/**
* jwt工具类
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "markerhub.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();
}
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());
}
}
基本的校验的路线完成之后,我们需要少量的基本信息配置yml
markerhub:
jwt:
# 加密秘钥
secret: f4e2e52034348f86b67cde581c0f9eb5
# token有效时长,7天,单位秒
expire: 604800
header: token
这是我做的一个demo的整合,具体配置还是根据自己的项目进行配置