至此,shiro和springboot和JWT的集成全部完毕,下面为完整代码。
第一:shiroConfig,整个shiro的配置类。
这里sesssion交给了shiro默认去管理,不在做过多的配置,一方面shiro的session时间够用了,另一方便,没必要把配置类搞得这么庞大,以前的shiro文章里特地讲了shiro的session,包括集成quatz。请看这里
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
@Bean("rememberCookie")
public SimpleCookie rememberCookie(){
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setHttpOnly(true);
simpleCookie.setName("remeberCookie");
simpleCookie.setMaxAge(360000);
return simpleCookie;
}
@Bean("cookieRememberMe")
public CookieRememberMeManager cookieRememberMe(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberCookie());
return cookieRememberMeManager;
}
/**
* 密码加密
* @return
*/
@Bean("credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("SHA-256");
hashedCredentialsMatcher.setHashIterations(20);
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
@Bean("shiroRealm")
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(credentialsMatcher());
return shiroRealm;
}
/**
* 这里session交给shiro默认管理,不去做详细配置
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//传入自定义shiroRealm
securityManager.setRealm(shiroRealm());
//这里要注意,setRememberMeManager里传入的类型是CookieRememberMeManager,不要搞错了
securityManager.setRememberMeManager(cookieRememberMe());
return securityManager;
}
/**
* 这里不设置loginUrl,传统的前后端不分离是可以配置,
* 因为这里前后端分离,前端没有获取到token自然会路由到登录页面进行登录操作,不再需要通过loginUrl定位到视图view,SpringBoot只负责后端
* 同时注意这里的filterMap中对于拦截路径的配置要以 / 开头,否则找不到对应的Controller,
* 切记:一定要把/** = authc放到最后!!!!!
* 2019-05-12补充说明:对于loginUrl最终还是需要的,filter里会进行是否为登录url的判断。
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,AuthFilter authFilter){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/user/login");
//设置自定义filter
Map filter = new HashMap<>();
filter.put("auth",authFilter);
shiroFilterFactoryBean.setFilters(filter);
//同时这里注意,使用LinkedHashMap来保证拦截器的顺序性
Map filterMap = new LinkedHashMap<>();
//登录逻辑这里不用anno,统一走自定义filter
//filterMap.put("/user/login","anon");
filterMap.put("/user/insert","auth");
filterMap.put("/**","auth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* Spring管理Shiro生命周期
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
}
第二:shiroRealm,承担着shiro的认证和授权工作。
/**
* 自定义shiroRealm
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
UserService service;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User user = 对应数据库查询操作,可根据自己的代码书写即可;
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
return simpleAuthenticationInfo;
}
}
第三:AuthFilter.shiro所有请求必须要走的过滤器
/**
* shiro过滤器
*/
@Component("authFilter")
public class AuthFilter extends FormAuthenticationFilter {
@Autowired
JwtUtil jwtUtil;
/**
* 判断token是否为空、过期
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String token = getRequestToken((HttpServletRequest) request);
if (ObjectUtils.isNull(token)){
return false;
}
if (StringUtils.isBlank(token)) {
throw new CustomException(jwtUtil.getHeader()+"不能为空", HttpStatus.SC_UNAUTHORIZED);
}
Claims claims = jwtUtil.parseToken(token);
if (ObjectUtils.isNull(claims) || jwtUtil.isTokenExpired(claims.getExpiration())) {
throw new CustomException(jwtUtil.getHeader()+"token过期",HttpStatus.SC_UNAUTHORIZED);
}
return true;
}
/**
* 上面的方法如果返回false,则接下来会执行这个方法,如果返回为true,则不会执行这个方法
* 判断是否为登录url,进一步判断请求是不是post
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return true;
}
}
return false;
}
/**
* 获取请求中的token,首先从请求头中获取,如果没有,则尝试从请求参数中获取
*
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request) {
String token = request.getHeader(jwtUtil.getHeader());
if (StringUtils.isBlank(token)) {
token = request.getParameter(jwtUtil.getHeader());
}
return token;
}
}
第四:JwtUtil.负责JWT的创建和校验。
/**
* JWT token 工具类,提供JWT生成,校验,工作
*/
@ConfigurationProperties(prefix = "dhb.jwt")
@Component
public class JwtUtil {
private Logger logger = LoggerFactory.getLogger(getClass());
private String secret;
private Long expire;
private String header;
/**
*
* 生成JWT token
* @param userId
* @return
*/
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.HS256, secret)
.compact();
}
/**
*
* 解析JWT token
* @param token
* @return
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.info("解析token出错");
return null;
}
}
/**
*
* 校验token是否过期
* @param expiprationTime
* @return
*/
public boolean isTokenExpired(Date expiprationTime){
return expiprationTime.before(new Date());
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
}
写在最后:虽然看起来这有这四大步,但是回头看看自己花了好长时间才走到现在这样。从一开始的认识shiro的架构,简单的基本使用,到实际运用中踩的坑,以及为什么要用token,不用session,以及舍弃配置复杂的session和quartz集成管理。这期间,每一步自己都在想,我为什么要用某一个技术,在实际编码中如何做到不让最后集成起来很臃肿。
推荐对于平时自己测试接口的时候,使用PostMan或者Yapi,千万不要为了测一个小小的接口又去写页面之类的,这样会使你脱离工作重心,最后啥啥都没干好。
还是那句话:既然要做,就做的细致一点,对得起自己!