我们做前后端分离需要使用Token方式做登录验证,即前端在header中加入token验证信息,后端做拦截器拦截前端发送的header中的token,然后再进行token验证。
而一个springboot项目中可能有多个登录身份,这个时候一个realm已经不能满足多个用户token进行验证,我在结合多realm做登录验证的方式,做出了多realmtoken验证。
思路:多realm需要增加一个标识来区分使用subject.login方法的时候寻找那个realm,同样,我想token验证也可以采用这样的方式。我在前端发送的header中多加入了一个字段User作为标识。
- 在jwtFilter拦截器中读取request中header的token字段和User字段,在通过自定义的jwtToken将token和User标识封装通过User字段与自定义的Realm前面类名匹配识别JwtToken进入哪个Realm中进行登录验证在Realm中进行token的验证将自定义Filter加载到shiro拦截器中。
相关类:
1、jwtFilterjwtFilter:token拦截器
JwtUtil:jwt工具类,用于生成token和验证token
JwtToken:封装带有User字段的Token信息
Realm:AdminSchoolTokenRealm、SystemTokenRealm、 UserTokenRealm
UserModularRealmAuthenticator:多realm匹配器
ShiroConfig
public class jwtFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest httpServletRequest=(HttpServletRequest) request;
if(!httpServletRequest.getMethod().equals("OPTIONS")){
System.out.println("httpServletRequest = " + httpServletRequest);
try{
String vituralType=httpServletRequest.getHeader("User");
String Token=httpServletRequest.getHeader("Token");
System.out.println("vituralType = " + vituralType);
System.out.println("Token = " + Token);
if(vituralType==null||Token==null)
return false;
if(!vituralType.equals("AdminSchool")&&!vituralType.equals("System")&&!vituralType.equals("User")){
return false;
}
Subject subject = SecurityUtils.getSubject();
JwtToken jwtToken=new JwtToken(vituralType,Token);
subject.login(jwtToken);
System.out.println("vituralType = " + vituralType);
System.out.println("Token = " + Token);
}catch(Exception e){
//验证失败,return false将进入到下面的方法中
return false;
}
}
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("application/json; charset=utf-8");
resp.setCharacterEncoding("utf-8");
PrintWriter writer=null;
try {
writer = resp.getWriter();
writer.write("jwttoken验证失败");
}finally {
if (writer != null) {
writer.close();
}
}
return false;
}
}
2、JwtUtil:
这些代码网上找的,找不多原作者了
public class JwtUtil {
//指定一个token过期时间(毫秒)
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; //7天
//注意这里的sercet不是密码,而是进行三件套(salt+MD5+1024Hash)处理密码后得到的凭证
//这里为什么要这么做,在controller中进行说明
public static String getJwtToken(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret); //使用密钥进行哈希
// 附带username信息的token
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date) //过期时间
.sign(algorithm); //签名算法
}
public static boolean verifyToken(String token, String username, String secret) {
try {
//根据密钥生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//效验TOKEN(其实也就是比较两个token是否相同)
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static boolean isExpire(String token){
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().getTime() < System.currentTimeMillis() ;
}
}
JwtToken
//将前端传入的header中的token和user信息封装成新的jwttoken
public class JwtToken implements AuthenticationToken {
public String getVirtualType() {
return virtualType;
}
//标识字段
private String virtualType;
private String Token;
public JwtToken(){}
public JwtToken(String virtualType,String Token){
this.virtualType=virtualType;
this.Token=Token;
}
@Override
public String getPrincipal() {
return Token;
}
@Override
public String getCredentials() {
return Token;
}
}
Realm:
AdminSchoolTokenRealm
public class AdminSchoolTokenRealm extends AuthorizingRealm {
@Autowired
private AdminschoolServerImpl adminschoolServer;
//重写一下这个方法,让自定义的jwttoken中的对象进来
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//授权器:还没有写
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证器:用来验证token,取代原来的账号密码认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("AdminSchoolTokenRealm");
JwtToken jwtToken=(JwtToken) token;
String Token=jwtToken.getCredentials();
if(Token.isEmpty()){
throw new AuthenticationException("Token为空");
}
String username=JwtUtil.getUsername(Token);;
String pwd =adminschoolServer.getPwdById(username);
if(pwd==null){
throw new AuthenticationException("账号不存在!");
}
if(!JwtUtil.verifyToken(Token,username,pwd)){
throw new AuthenticationException("密码错误!");
}
if(JwtUtil.isExpire(Token)){
throw new AuthenticationException("Token过期");
}
return new SimpleAuthenticationInfo(username,Token,this.getName());
}
}
下面的两个realm与上面的基本类似
SystemTokenRealm
UserTokenRealm
UserModularRealmAuthenticatorpublic class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
//强制转换用户token
JwtToken userToken = (JwtToken) authenticationToken;
Collection realms = getRealms();
String virtualType = userToken.getVirtualType();
Collection typeRealms = new ArrayList<>();
for (Realm realm : realms) {
if (realm.getName().contains(virtualType.toString())) {// 注:这里使用类名包含枚举,区分realm
typeRealms.add(realm);
}
}
if (typeRealms.size() == 1) {
return doSingleRealmAuthentication(typeRealms.iterator().next(), userToken);
} else {
return doMultiRealmAuthentication(typeRealms, userToken);
}
}
}
ShrioConfig
@Configuration
public class ShiroConfig {
private final long SessionTime=7 * 24 * 60 * 60 * 1000;
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
//设置安全管理器
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
//添加shiro的内置过滤器
// Map filter=new HashMap<>();
// filter.put("authc",new ShiroLoginFilter());
linkedHashMap filtermapd = new linkedHashMap<>();
filtermapd.put("jwt", new jwtFilter());
bean.setFilters(filtermapd);
Map filterMap=new linkedHashMap<>();
filterMap.put("/AschUser/Login","anon");
filterMap.put("/AsysUser/Login","anon");
filterMap.put("/User/Login","anon");
filterMap.put("
@Qualifier("adminSchoolTokenRealm") AdminSchoolTokenRealm adminSchoolTokenRealm,
@Qualifier("systemTokenRealm") SystemTokenRealm systemTokenRealm,
@Qualifier("userTokenRealm") UserTokenRealm userTokenRealm,
@Qualifier("authenticator") UserModularRealmAuthenticator authenticator){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setAuthenticator(authenticator);
List realms =new ArrayList<>();
// realms.add(adminSystemRealm);
// realms.add(userRealm);
// realms.add(adminSchoolRealm);
realms.add(adminSchoolTokenRealm);
realms.add(systemTokenRealm);
realms.add(userTokenRealm);
securityManager.setRealms(realms);
return securityManager;
}
@Bean
public AdminSchoolTokenRealm adminSchoolTokenRealm(){return new AdminSchoolTokenRealm();}
@Bean
public SystemTokenRealm systemTokenRealm(){return new SystemTokenRealm();}
@Bean
public UserTokenRealm userTokenRealm(){return new UserTokenRealm();}
@Bean
public SystemRealm adminSystemRealm(){
return new SystemRealm();
}
@Bean
public UserRealm userRealm(){return new UserRealm();}
@Bean
public AdminSchoolRealm adminSchoolRealm(){return new AdminSchoolRealm();}
@Bean
public UserModularRealmAuthenticator authenticator(){
return new UserModularRealmAuthenticator();
}
@Bean
protected CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
}
三、演示验证
1、登录
由于token验证使用了realm本身的账号密码验证,所以我使用了原始的登录,在server中写的方法,接口方法直接调用就可以
public ResultType Login(String Username, String Pwd){
if(Username==null||Pwd==null){
return ResultType.fail("账号或密码为空",101);
}
String s = this.getPwdById(Username);
if(s==null){
return ResultType.fail("账号不存在",101);
}
if(!s.equals(Pwd)){
return ResultType.fail("密码错误",101);
}
Map map= new HashMap<>();
map.put("Token",JwtUtil.getJwtToken(Username,Pwd));
return ResultType.success("登录成功!",map);
}
登录成功会返回token和登录成功提示
如果非登录接口请求未携带token和user字段,直接返回500
如果携带token信息,会返回对应的信息
这样做出来,前端只要在每次请求的请求头中加入两个字段即可,不会报跨域问题。
我是做毕设的时候遇到这样的问题,查了一些资料,做出来的东西也很稚嫩,希望以后能够学到跟牛的技术。



