因公司项目需求,做一个微服务项目,相比于单体架构,难免涉及到跨域登录的问题,你可能是在一个模块写了登录操作,但是其他模块的请求未进行拦截,或者是拦截了请求,老是提示未登录,这个时候就需要进行多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跟踪下源码看看到底是哪里的错误就行了(具体什么错误不贴了,我还要把代码改回去复现,挺麻烦,你们遇到了就能看明白)。