最近想试试Shiro 和 JWT 集成,说实话,与传统的session机制相比,有优点也有缺点,这就需要你们自行斟酌使用啦,就说说遇到的坑而已。
先说需求,密码加密储存,JWT。这就需要两种认证方式。
别的不说先上pom.xml,都是基本的starter,用过spring boot 的都应该熟悉
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.2.RELEASE
com.manage
competition
0.0.1-SNAPSHOT
competition
Manage project for Competition
1.8
org.springframework.boot
spring-boot-starter-web
com.alibaba
druid-spring-boot-starter
1.1.10
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
5.1.6
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-tomcat
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-cache
org.apache.shiro
shiro-spring-boot-web-starter
1.4.0
com.auth0
java-jwt
3.7.0
org.springframework.boot
spring-boot-maven-plugin
自定义Realm
两种验证方式,一种密码登录: 暂时没用到这种方式的权限认证也就没写逻辑了
package com.manage.competition.shiro;
import com.manage.competition.common.Const;
import com.manage.competition.entity.Permission;
import com.manage.competition.entity.Role;
import com.manage.competition.entity.User;
import com.manage.competition.repository.PermissionRepository;
import com.manage.competition.repository.RoleRepository;
import com.manage.competition.repository.UserRepository;
import com.manage.competition.util.JwtUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* Create with IDEA
*
* @Author:Vantcy
* @Date: Create in 16:30 2019/1/25
* @Description: 普通的自定义realm
*/
public class AuthRealm extends AuthorizingRealm{
@Autowired
private UserRepository userRepository;
/**
* 此Realm只支持JwtToken
* @return
*/
@Override
public Class> getAuthenticationTokenClass() {
return UsernamePasswordToken.class;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————————————————————————————auth身份认证方法————————————————————————————");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库获取对应用户名密码的用户
User user = userRepository.findByUsername(token.getUsername());
if (user == null){
throw new UnknownAccountException();
}
//是否激活
if(user.getStatus().equals(Const.Status.DISABLE)){
throw new DisabledAccountException();
}
//是否锁定
if(user.getStatus().equals(Const.Status.ILLEGAL)) {
throw new LockedAccountException();
}
if(user.getStatus().equals(Const.Status.ENABLE)){
//盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername());
return new SimpleAuthenticationInfo(user, user.getPassword(),
credentialsSalt, getName());
}
return null;
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————————————————————————————auth权限认证————————————————————————————");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
return info;
}
}
一种token验证:这里有个坑就是一定要这个JwtRealm只支持认证JwtToken,不然会报错
package com.manage.competition.shiro;
import com.google.common.collect.Sets;
import com.manage.competition.common.Const;
import com.manage.competition.entity.Permission;
import com.manage.competition.entity.Role;
import com.manage.competition.entity.User;
import com.manage.competition.repository.PermissionRepository;
import com.manage.competition.repository.RoleRepository;
import com.manage.competition.repository.UserRepository;
import com.manage.competition.util.JwtUtil;
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.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Create with IDEA
*
* @Author: gitee.com/KamisamaXX
* @Date: Create in 15:38 2019/1/31
* @Description: 基于JWT( JSON WEB TOKEN)的认证域
*/
public class JwtRealm extends AuthorizingRealm {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PermissionRepository permissionRepository;
/**
* 此Realm只支持JwtToken
* @return
*/
@Override
public Class> getAuthenticationTokenClass() {
return JwtToken.class;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————————————————————————jwt身份认证方法————————————————————————");
String token = (String) authenticationToken.getCredentials();
String username = JwtUtil.getUsername(token);
if (username == null || !JwtUtil.verify(token,username)) {
throw new AuthenticationException("token认证失败!");
}
User user = userRepository.findByUsername(username);
if (user == null) {
throw new AuthenticationException("该用户不存在!");
}
if (user.getStatus() != Const.Status.ENABLE) {
throw new AuthenticationException("该用户已被删除或封号!");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
/**
* 授权,JWT已包含访问主张只需要解析其中的主张定义就行了
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————————————————————————————jwt权限认证————————————————————————————");
String username = JwtUtil.getUsername(principalCollection.toString());
User user = userRepository.findByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//因为addRoles和addStringPermissions方法需要的参数类型是Collection
//所以先创建两个collection集合
Collection rolesCollection = new HashSet();
Collection permissionsCollection = new HashSet();
//获取user的Role的List集合
List roles = roleRepository.findAllByUserId(user.getId());
for (Role role:
roles) {
//将每一个role的name装进collection集合
rolesCollection.add(role.getRole());
List permissions = permissionRepository.findAllByRoleId(role.getId());
for (Permission permission:
permissions) {
//将每一个permission的name装进collection集合
permissionsCollection.add(permission.getPerm());
}
//为用户授予权限
info.addStringPermissions(permissionsCollection);
}
//为用户授予角色
info.addRoles(rolesCollection);
return info;
}
}
Token也简单实现一下类,然后多加了一个字段而已,这个字段就是来储存token的
package com.manage.competition.shiro;
import lombok.Getter;
import org.apache.shiro.authc.AuthenticationToken;
/**
* Create with IDEA
*
* @Author: gitee.com/KamisamaXX
* @Date: Create in 10:52 2019/1/30
* @Description: JWT令牌
*/
@Getter
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1984408664001215860L;
/**
* token
*/
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
然后就是要自己定义一个Filter去拦截header里带有token字段的那些请求,然后去验证一下token是否有效。
package com.manage.competition.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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;
import java.io.IOException;
import java.net.URLEncoder;
/**
* Create with IDEA
*
* @Author: gitee.com/KamisamaXX
* @Date: Create in 10:45 2019/1/30
* @Description: preHandle->isAccessAllowed->isLoginAttempt->executeLogin
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 如果带有 token,则对 token 进行检查,否则直接通过
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue){
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
System.out.println(e);
//throw new UnsupportedTokenException("token 错误");
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Token");
return token != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws AuthenticationException{
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Token");
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
JwtToken jwtToken = new JwtToken(token);
// 如果没有抛出异常则代表登入成功,返回true
SecurityUtils.getSubject().login(jwtToken);
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);
}
}
shiroConfig,在这里自定义一些shiro的配置项,需要定义一个hashedCredentialsMatcher去配置密码加密方式,注入两个Realm,securityManager里要设置取消session机制,注入filter规则等等都有相应的注释
package com.manage.competition.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.DefaultSessionManager;
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.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.*;
/**
* Create with IDEA
*
* @Author:Vantcy
* @Date: Create in 15:54 2019/1/25
* @Description:
*/
@Configuration
@Slf4j
public class ShiroConfig {
/**
* 密码校验规则HashedCredentialsMatcher
* 这个类是为了对密码进行编码的 ,
* 防止密码在数据库里明码保存 , 当然在登陆认证的时候 ,
* 这个类也负责对form里输入的密码进行编码
* 处理认证匹配处理器:如果自定义需要实现继承HashedCredentialsMatcher
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//指定加密方式为MD5
credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
//加密次数
credentialsMatcher.setHashIterations(1024);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
log.info("hashedMatch注入成功");
return credentialsMatcher;
}
/**
* Sha256Hash 身份认证 realm;
*
* 必须写这个类,并加上 @Bean 注解,目的是注入 Realm,
* 否则会影响 Realm类 中其他类的依赖注入
*/
@Bean(name = "authRealm")
public AuthRealm authRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
AuthRealm authRealm = new AuthRealm();
authRealm.setAuthenticationCachingEnabled(true);
authRealm.setCredentialsMatcher(matcher);
authRealm.setCachingEnabled(true);
authRealm.setCacheManager(new MemoryConstrainedCacheManager());
log.info("TwtRealm类注入成功");
return authRealm;
}
/**
* JWT Token身份认证 realm;
*
* 必须写这个类,并加上 @Bean 注解,目的是注入 Realm,
* 否则会影响 Realm类 中其他类的依赖注入
*/
@Bean(name = "jwtRealm")
public JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm();
jwtRealm.setAuthenticationCachingEnabled(true);
jwtRealm.setCachingEnabled(true);
jwtRealm.setCacheManager(new MemoryConstrainedCacheManager());
log.info("TwtRealm类注入成功");
return jwtRealm;
}
/**
* 注入 securityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm, @Qualifier("authRealm") AuthRealm authRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
StatelessSubjectFactory statelessSubjectFactory= new StatelessSubjectFactory();
DefaultSessionManager defaultSessionManager = new DefaultSessionManager();
defaultSessionManager.setSessionValidationSchedulerEnabled(false);
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
Collection realms = new HashSet<>();
realms.add(jwtRealm);
realms.add(authRealm);
// 设置realm.
securityManager.setRealms(realms);
securityManager.setSubjectFactory(statelessSubjectFactory);
securityManager.setSessionManager(defaultSessionManager);
securityManager.setSubjectDAO(defaultSubjectDAO);
log.info("SecurityManager类注入成功");
return securityManager;
}
/**
* 先走 filter ,然后 filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
*/
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
// 设置拦截器
Map filterChainDefinitionMap = new LinkedHashMap<>();
//开放登陆接口
filterChainDefinitionMap.put("/user/login", "anon");
//开放注册接口
filterChainDefinitionMap.put("/user/register", "anon");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
log.info("解决Shiro注解bug类注入成功");
return defaultAdvisorAutoProxyCreator;
}
/**
* 配置shiro跟spring的关联
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
log.info("Shiro关联spring类注入成功");
return advisor;
}
/**
* lifecycleBeanPostProcessor是负责生命周期的 , 初始化和销毁的类
* (可选)
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
log.info("lifecycleBeanPostProcessor生命周期类注入成功");
return new LifecycleBeanPostProcessor();
}
}
这里涉及到两个认证方式,authRealm 和 JwtRealm ,第一个是加入了hashMatch,为了和数据库里加密的密码比对,第二种为了验证token,而在shiro中,多realm匹配会有一个问题,
直接限定死了能认证的token类型,虽然说简单省事,但是扩展性低。