Shiro+Jwt实现多Realm跨域登录认证

因公司项目需求,做一个微服务项目,相比于单体架构,难免涉及到跨域登录的问题,你可能是在一个模块写了登录操作,但是其他模块的请求未进行拦截,或者是拦截了请求,老是提示未登录,这个时候就需要进行多realm登录认证(同事说security+Oauth2也可以解决,不过我对这个不是很熟悉,就用的shiro)

使用多Realm登录认证的时候首先要明白一个问题,什么时候走哪个realm,比如,用户直接登录肯定是用用户名和密码登录,访问其他模块就需要用JwtToken登录,这里用到了 AuthenticatingRealm中的getAuthenticationTokenClass()方法来获取登录Token的类型,以匹配realm。

GitHub地址:https://github.com/LI-DAI/springboot_demo,只看springboot_shiro部分即可

下面贴出shiro的配置类,ShiroConfig


package com.hiynn.capability.common.config;

import com.hiynn.capability.common.shiro.filter.CustomAuthenticationFilter;
import com.hiynn.capability.common.shiro.realm.CustomShiroRealm;
import com.hiynn.capability.common.shiro.realm.TokenValidateRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
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.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author lidai
 * @date 2018/12/24 14:59
 */
@Configuration
public class ShiroConfig {

    public static final String LOGIN_URL = "/login";

    /**
     * 管理bean生命周期
     *
     * @return
     */
    @Bean
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl(LOGIN_URL);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map filter = new LinkedHashMap<>();
        filter.put("customFilter", new CustomAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filter);
        Map filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/logout", "logout");
        this.getAllowedUri(filterChainDefinitionMap);
        filterChainDefinitionMap.put("/**", "customFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    public void getAllowedUri(Map filterChainDefinitionMap) {
        filterChainDefinitionMap.put("/apiService/all", "anon");
        filterChainDefinitionMap.put("/apiService/", "anon");
        filterChainDefinitionMap.put("/apiServiceGroup/**", "anon");
        filterChainDefinitionMap.put("/CapBackService/getAll", "anon");
        filterChainDefinitionMap.put("/CapBackServiceGroup/insert", "anon");
        filterChainDefinitionMap.put("/CapBackService/insert", "anon");
        filterChainDefinitionMap.put("/CapBackServiceGroup/getSameGroupName", "anon");
        filterChainDefinitionMap.put("/CapBackServiceGroup/getGroupByGroupName", "anon");
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//        securityManager.setRealm(customShiroRealm());
        securityManager.setRealms(Arrays.asList(customShiroRealm(), tokenValidateRealm()));
        return securityManager;
    }

    @Bean
    public CustomShiroRealm customShiroRealm() {
        CustomShiroRealm shiroRealm = new CustomShiroRealm();
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    /**
     * 此处realm登录必须开启缓存,登录时会对这个参数进行判断,如果为false,则认证不通过
     *
     * @return
     */
    @Bean
    public TokenValidateRealm tokenValidateRealm() {
        TokenValidateRealm tokenValidateRealm = new TokenValidateRealm();
        tokenValidateRealm.setAuthenticationCachingEnabled(true);
//        tokenValidateRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return tokenValidateRealm;
    }

    /**
     * 凭证匹配,加密算法
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    /**
     * 开启shiro 注解支持
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 开启shiro授权注解,若上面Bean未生效则使用此Bean
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

}

然后时WebConfiguration,配置跨域,其实这个我感觉有没有都是可以的,当然,有也没什么问题


package com.hiynn.capability.common.config;

import com.hiynn.capability.common.service.UserService;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author lidai
 * @date 2018/12/25 15:53
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer{

    @Bean
    public UserService userService(){
        return new UserService();
    }

//    @Bean
//    public FilterRegistrationBean corsFilter() {
//        FilterRegistrationBean filterBean = new FilterRegistrationBean<>();
//        CorsConfiguration corsConfiguration = new CorsConfiguration();
//        corsConfiguration.addAllowedOrigin("*");
//        corsConfiguration.addAllowedHeader("*");
//        corsConfiguration.addAllowedMethod("*");
//        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//        source.registerCorsConfiguration("/**", corsConfiguration);
//        filterBean.setFilter(new CorsFilter(source));
//        return filterBean;
//    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
//        registry.addMapping("*");
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("*")
                .allowedHeaders("*")
                .maxAge(3600);
    }

}

然后就是一个登录核心配置类,你想在哪个模块进行登录拦截,直接启动类中引入代码即可

package com.hiynn.capability.common.config;

import org.springframework.context.annotation.Import;

/**
 * @author lidai
 * @date 2018/12/25 17:17
 *
 * 登录核心配置类,需要进行登录拦截的模块直接导入{@code @Import(CustomCoreConfiguration.class)}即可
 */
@Import({ShiroConfig.class,WebConfiguration.class})
public class CustomCoreConfiguration {

}

以上这些都是基本配置,没有什么好说的,下面看一下自定义的filter,我是继承的AuthenticatingFilter这个过滤器,当然你可能不想继承这个,换一个也没问题,一般来说继承AccessControlFilter或者这个类的子类都可以的,下图中CustomAuthenticationFilter这个类就是我自定义的,代码也贴上来


package com.hiynn.capability.common.shiro.filter;

import com.alibaba.fastjson.JSONObject;
import com.hiynn.capability.common.entity.Result;
import com.hiynn.capability.common.shiro.token.ValidateToken;
import com.hiynn.capability.common.shiro.utils.JwtHelper;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lidai
 * @date 2018/12/25 15:07
 */
@Component
public class CustomAuthenticationFilter extends AuthenticatingFilter {

    public static final String USERNAME = "username";
    public static final String PASSWORD = "password";
    public static final String TOKEN = "Token";

    private ValidateToken validateToken;

    public CustomAuthenticationFilter(){
        this.validateToken = new ValidateToken();
    }

    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        String username;
        String password;
        if (servletRequest.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            //非表单登录
            try (BufferedReader reader = servletRequest.getReader()) {
                String body = reader.lines().reduce(String::concat).orElseThrow(IllegalArgumentException::new);
                JSONObject object = JSONObject.parseObject(body);
                username = object.getString(USERNAME);
                password = object.getString(PASSWORD);
                if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                    return createToken(username, password, servletRequest, servletResponse);
                }
            }
        } else {
            //表单登录
            username = servletRequest.getParameter(USERNAME);
            password = servletRequest.getParameter(PASSWORD);
            if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                return createToken(username, password, servletRequest, servletResponse);
            }
        }
        throw new IllegalArgumentException("请求Content-type错误");
    }

    /**
     * 未通过处理
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (isLoginRequest(request, response)) {
            if (!request.getMethod().equalsIgnoreCase(POST_METHOD)) {
                setResponse(Result.build().fail("Method is not Allowed:" + request.getMethod()), response);
                return false;
            }
            return executeLogin(servletRequest, servletResponse);
        }
        setResponse(Result.build().unauthenticated(), servletResponse);
        return false;
    }

    /**
     * 是否允许通过
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        if (isLoginRequest(request, response)) {
            return false;
        }
        String jwtToken = httpServletRequest.getHeader(TOKEN);
        if (!StringUtils.hasText(jwtToken)) {
            jwtToken = httpServletRequest.getParameter(TOKEN);
            if(!StringUtils.hasText(jwtToken)){
                return false;
            }
        }
//        if(StringUtils.hasText(jwtToken)){
//            try {
//                JwtHelper.parseJWT(jwtToken);
//            } catch (Exception e) {
//                throw new AuthenticationException("无效的Token");
//            }
//        }
        if(!validateToken.validateToken(jwtToken,request)){
            return false;
        }
        return true;
    }

    /**
     * 登录成功执行方法
     *
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        String jwtToken = JwtHelper.createJWT((String) token.getPrincipal());
        Map map = new HashMap<>();
        map.put("Token", jwtToken);
        Result result = Result.build().success("登录成功", map);
        setResponse(result, response);
        return false;
    }

    /**
     * 登录失败执行方法
     *
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        Result result = Result.build().success("登录失败");
        setResponse(result, response);
        return false;
    }

    private void setResponse(Result result, ServletResponse response) {
        //解决中文乱码
        response.setContentType("application/json;charset=UTF-8");
        try {
            response.getWriter().write(JSONObject.toJSONString(result));
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }

}

简单说以下这个过滤器,请求进来的时候首先会进入isAccessAllowed这个方法,此方法代表是否允许请求通过,我在其中判断了以下,是否是登录请求的判断,还有对token的验证,允许通过返回true。如果允许通过,则跳过shiro执行的相关内容,直接跳到你想访问的地方去。如果不允许访问,则下面会走到onAccessDenied方法,我在这里对登录 请求进行了登录操作,还有一个createToken方法是必须继承的,用username和password创建token即可,其余的像OnLoginSuccess和onLoginFailure方法要不要都是可以的

然后看一下第一个Realm,使用username和password进行登录的Realm


package com.hiynn.capability.common.shiro.realm;

import com.hiynn.capability.common.entity.UserInfo;
import com.hiynn.capability.common.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
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;

/**
 * @author lidai
 * @date 2018/12/24 15:37
 */
public class CustomShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Override
    public Class getAuthenticationTokenClass() {
        return super.getAuthenticationTokenClass();
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        UserInfo user = userService.getUserByUsername(upToken.getUsername());
        if(null == user){
            throw new UnknownAccountException("用户名或密码错误");
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()),
                getName());

        return simpleAuthenticationInfo;
    }
}

没啥好说的,shiro的基本登录操作

接下来看自定义的JwtToken,这里也可以实现其他类,只要是AuthenticationToken的子类就可以


package com.hiynn.capability.common.shiro.token;

import org.apache.shiro.authc.HostAuthenticationToken;

/**
 * @author lidai
 * @date 2018/12/28 11:57
 */
public class JwtToken implements HostAuthenticationToken {

    private String token;
    private String host;

    public JwtToken(){}

    public JwtToken(String token, String host) {
        this.token = token;
        this.host = host;
    }

    @Override
    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return Boolean.TRUE;
    }
}

JWT工具类


package com.hiynn.capability.common.shiro.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lidai
 * @date 2018/12/28 13:35
 */
public class JwtHelper {

    private static final String ISSUER="user";//签发者

    private static final String SECRET="secretTest";//密钥

    private static final Long TTLMillis=12*60*60*1000L;//过期时间12小时

    public static final String USERNAME="username";

    /**
     * 创建jwt
     * @param username
     * @return
     */
    public static String createJWT(String username){
        Map claims=new HashMap<>();
        claims.put(USERNAME,username);
        Long currentTimeMillis=System.currentTimeMillis();
        JwtBuilder builder= Jwts.builder()
                .setClaims(claims)
                .setIssuer(ISSUER)
                .signWith(SignatureAlgorithm.HS512,SECRET.getBytes())
                .setIssuedAt(new Date(currentTimeMillis))
                .setExpiration(new Date(currentTimeMillis+TTLMillis));
        return builder.compact();
    }

    /**
     * 解析jwt
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET.getBytes())
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }

}

仔细看过滤器的话,会发现我在isAccessAllowed方法中对JwtToken进行了验证,所谓的验证就是使用JwtToken进行登录,当然你也可以不登录,只要验证下JwtToken的正确性就返回true,这样也是可以的,不过我认为这是一种假得登录方式,如果你要在当前模块获取登录用户的话是获取不到的


package com.hiynn.capability.common.shiro.token;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;

import javax.servlet.ServletRequest;

/**
 * @author lidai
 * @date 2018/12/28 13:37
 */
public class ValidateToken {

    public boolean validateToken(String token, ServletRequest request){
        AuthenticationToken authenticationToken=this.createToken(token,request);
        try{
            Subject subject = SecurityUtils.getSubject();
            subject.login(authenticationToken);
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return  false;
        }
    }

    public AuthenticationToken createToken(String token, ServletRequest request){
        return new JwtToken(token,request.getRemoteHost());
    }
}

这里看第二个realm


package com.hiynn.capability.common.shiro.realm;

import com.hiynn.capability.common.entity.UserInfo;
import com.hiynn.capability.common.service.UserService;
import com.hiynn.capability.common.shiro.token.JwtToken;
import com.hiynn.capability.common.shiro.utils.JwtHelper;
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.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * @author lidai
 * @date 2018/12/28 14:49
 * 

* 此realm为使用token时对其进行登录认证 */ public class TokenValidateRealm extends AuthenticatingRealm { @Autowired private UserService userService; /** * 调用{@code doGetAuthenticationInfo(AuthenticationToken)}之前会shiro会调用{@code supper.supports(AuthenticationToken)} * 来判断该realm是否支持对应类型的AuthenticationToken,如果相匹配则会走此realm * * @return */ @Override public Class getAuthenticationTokenClass() { return JwtToken.class; } /** * 使用JwtToken登录的时候并未使用MD5加密,接下来会走到{@link SimpleCredentialsMatcher}的{@code doCredentialsMatch}方法进行证书比对 * 也就是此处传入的{@code Boolean.TRUE}与{@link JwtToken}中获取的证书相比较 * 之所以不加密是因为无法获取用户加密前的密码,一旦经{@link HashedCredentialsMatcher}进行证书比对则报错 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { JwtToken jwtToken = (JwtToken) token; String jwt = (String) jwtToken.getPrincipal(); String username; try { username = (String) JwtHelper.parseJWT(jwt).get(JwtHelper.USERNAME); } catch (AuthenticationException e) { throw new AuthenticationException(); } UserInfo userInfo = userService.getUserByUsername(username); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( userInfo, Boolean.TRUE, getName()); return simpleAuthenticationInfo; } }

这个类的代码注释中我都写的很清楚,很容易就能看懂

最后贴以下用户使用的登录类,这个登录类定义了一个/login的路径,只要访问这个路径自定义的CustomAuthenticationFilter就会拦截进行登录操作,这些在CustomAuthenticationFilter里写的很清楚了


package com.hiynn.provider.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lidai
 * @date 2018/12/24 16:11
 */
@RestController
@CrossOrigin
@RequestMapping("/login")
public class LoginController {

}

注意下多realm登录的时候有个坑就是无论你是什么错误,他都会报一个错误,所以遇到这种问题的时候debug跟踪下源码看看到底是哪里的错误就行了(具体什么错误不贴了,我还要把代码改回去复现,挺麻烦,你们遇到了就能看明白)。

你可能感兴趣的:(springboot,Shiro,Jwt,多Realm,跨域)