shiro 改造成 jwt token 认证后(如果自定义了 shiroFilter 并且在 onAccessAllow 中加上了 executeLogin 的逻辑可能会避过这个坑)因为 session 被禁用的缘故,每次请求进来后的 subject 中是没有用户信息和权限信息的,所以在做除了登录之外的操作时,后台接口加了注解时会报无权限和未授权的问题。
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
.............
这里会根据 request 和 response 来创建 subject 对象,也就是说如果开启了 session,那么这里创建 subject 时会先从 session 中将用户的权限信息放入到 subject 中去,也就是此时的 subject 是有权限信息的(session 未过期的前提下)。
org.apache.shiro.subject.support.DelegatingSubject#execute(java.util.concurrent.Callable):
public <V> V execute(Callable<V> callable) throws ExecutionException {
Callable<V> associated = associateWith(callable);
try {
return associated.call();
} catch (Throwable t) {
throw new ExecutionException(t);
}
}
org.apache.shiro.subject.support.SubjectCallable#call:
public V call() throws Exception {
try {
threadState.bind();
return doCall(this.callable);
} finally {
threadState.restore();
}
}
org.apache.shiro.subject.support.SubjectThreadState#bind:
public void bind() {
SecurityManager securityManager = this.securityManager;
if ( securityManager == null ) {
//try just in case the constructor didn't find one at the time:
securityManager = ThreadContext.getSecurityManager();
}
this.originalResources = ThreadContext.getResources();
ThreadContext.remove();
ThreadContext.bind(this.subject);
if (securityManager != null) {
ThreadContext.bind(securityManager);
}
}
org.apache.shiro.util.ThreadContext#bind(org.apache.shiro.subject.Subject):
public static void bind(Subject subject) {
if (subject != null) {
put(SUBJECT_KEY, subject);
}
}
org.apache.shiro.util.ThreadContext#put:
public static void put(Object key, Object value) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
if (value == null) {
remove(key);
return;
}
ensureResourcesInitialized();
resources.get().put(key, value);
if (log.isTraceEnabled()) {
String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +
key + "] to thread " + "[" + Thread.currentThread().getName() + "]";
log.trace(msg);
}
}
到这里就将当前线程的 ThreadLocal 中放入了 Subject 对象,并建立绑定关系。key 为 org.apache.shiro.util.ThreadContextSUBJECTKEY,value 为 WebDelegatingSubject 对象。
Subject subject = SecurityUtils.getSubject();
org.apache.shiro.SecurityUtils#getSubject:
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
org.apache.shiro.util.ThreadContext#getSubject:
public static Subject getSubject() {
return (Subject) get(SUBJECT_KEY);
}
org.apache.shiro.util.ThreadContext#get:
public static Object get(Object key) {
if (log.isTraceEnabled()) {
String msg = "get() - in thread [" + Thread.currentThread().getName() + "]";
log.trace(msg);
}
Object value = getValue(key);
if ((value != null) && log.isTraceEnabled()) {
String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" +
key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]";
log.trace(msg);
}
return value;
}
private static Object getValue(Object key) {
Map<Object, Object> perThreadResources = resources.get();
return perThreadResources != null ? perThreadResources.get(key) : null;
}
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
获取到与当前线程绑定的 subject 对象。
subject.login(token):
org.apache.shiro.subject.support.DelegatingSubject#login:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
org.apache.shiro.mgt.DefaultSecurityManager#login:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
SubjectContext context = createSubjectContext();
context.setAuthenticated(true);
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return createSubject(context);
}
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);
//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);
//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
可见,这里是对线程绑定的 subject 进行包装后创建了一个新的 subject 对象,并通过 save(subject)进行处理(将 subject 放入 session 中),具体的处理逻辑为: org.apache.shiro.mgt.DefaultSubjectDAO#save:
public Subject save(Subject subject) {
if (isSessionStorageEnabled(subject)) {
saveToSession(subject);
} else {
log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
"authentication state are expected to be initialized on every request or invocation.", subject);
}
return subject;
}
我们是禁用 session 的,所以这里的 isSessionStorageEnabled 为 false,也就是说 save 方法没有操作。
这里稍微提下 saveToSession 的操作:
protected void saveToSession(Subject subject) {
//performs merge logic, only updating the Subject's session if it does not match the current state:
mergePrincipals(subject);
mergeAuthenticationState(subject);
}
protected void mergePrincipals(Subject subject) {
//merge PrincipalCollection state:
PrincipalCollection currentPrincipals = null;
//SHIRO-380: added if/else block - need to retain original (source) principals
//This technique (reflection) is only temporary - a proper long term solution needs to be found,
//but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
//
//A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
if (subject.isRunAs() && subject instanceof DelegatingSubject) {
try {
Field field = DelegatingSubject.class.getDeclaredField("principals");
field.setAccessible(true);
currentPrincipals = (PrincipalCollection)field.get(subject);
} catch (Exception e) {
throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
}
}
if (currentPrincipals == null || currentPrincipals.isEmpty()) {
currentPrincipals = subject.getPrincipals();
}
Session session = subject.getSession(false);
if (session == null) {
if (!isEmpty(currentPrincipals)) {
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
// otherwise no session and no principals - nothing to save
} else {
PrincipalCollection existingPrincipals =
(PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (isEmpty(currentPrincipals)) {
if (!isEmpty(existingPrincipals)) {
session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
}
// otherwise both are null or empty - no need to update the session
} else {
if (!currentPrincipals.equals(existingPrincipals)) {
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
// otherwise they're the same - no need to update the session
}
}
}
protected void mergeAuthenticationState(Subject subject) {
Session session = subject.getSession(false);
if (session == null) {
if (subject.isAuthenticated()) {
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
}
//otherwise no session and not authenticated - nothing to save
} else {
Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
if (subject.isAuthenticated()) {
if (existingAuthc == null || !existingAuthc) {
session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
}
//otherwise authc state matches - no need to update the session
} else {
if (existingAuthc != null) {
//existing doesn't match the current state - remove it:
session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
}
//otherwise not in the session and not authenticated - no need to update the session
}
}
}
这里的操作无非就是往 session 里塞入键值对,键值对中的 key 为:
private static final String SECURITY_MANAGER = DefaultSubjectContext.class.getName() + ".SECURITY_MANAGER";
private static final String SESSION_ID = DefaultSubjectContext.class.getName() + ".SESSION_ID";
private static final String AUTHENTICATION_TOKEN = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_TOKEN";
private static final String AUTHENTICATION_INFO = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_INFO";
private static final String SUBJECT = DefaultSubjectContext.class.getName() + ".SUBJECT";
private static final String PRINCIPALS = DefaultSubjectContext.class.getName() + ".PRINCIPALS";
private static final String SESSION = DefaultSubjectContext.class.getName() + ".SESSION";
private static final String AUTHENTICATED = DefaultSubjectContext.class.getName() + ".AUTHENTICATED";
private static final String HOST = DefaultSubjectContext.class.getName() + ".HOST";
public static final String SESSION_CREATION_ENABLED = DefaultSubjectContext.class.getName() + ".SESSION_CREATION_ENABLED";
/**
* The session key that is used to store subject principals.
*/
public static final String PRINCIPALS_SESSION_KEY = DefaultSubjectContext.class.getName() + "_PRINCIPALS_SESSION_KEY";
/**
* The session key that is used to store whether or not the user is authenticated.
*/
public static final String AUTHENTICATED_SESSION_KEY = DefaultSubjectContext.class.getName() + "_AUTHENTICATED_SESSION_KEY";
禁用了 session 后,subject 是没有存放在 session 中的,所以当调用 subject.login 方法后的这个 subject 是有权限信息的,其他请求进入时创建的 subject 则不然。
这时我们再回头看一眼 login 方法里调用的 authenticate 方法 org.apache.shiro.mgt.AuthenticatingSecurityManager#authenticate:
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
调用的是 org.apache.shiro.authc.AbstractAuthenticator#authenticate:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
//Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more
//severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate:
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +
"error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
if (log.isWarnEnabled())
log.warn(msg, t);
}
try {
notifyFailure(token, ae);
} catch (Throwable t2) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. " +
"Please check your AuthenticationListener implementation(s). Logging sending exception " +
"and propagating original AuthenticationException instead...";
log.warn(msg, t2);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
notifySuccess(token, info);
return info;
}
这里我们继续看 doAuthenticate 即 org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
doGetAuthenticationInfo 调用的是自定义 realm 的 doGetAuthenticationInfo 实现:
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
StatelessToken statelessToken = (StatelessToken) token;
String username = (String) statelessToken.getPrincipal();
//密码,shiro会根据token的credentials进行加密然后与SimpleAuthenticationInfo的第二个参数进行比较
//String loginedToken = (String) statelessToken.getCredentials();
//通过校验
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username,
statelessToken.getToken(),
ByteSource.Util.bytes(statelessToken.getSalt()),
getName()
);
return authenticationInfo;
}
我们再合起来看下,登录时的代码简化如下:
AuthenticationInfo info = authenticate(token);
Subject loggedIn = createSubject(token, info, subject);
这时生成的 subject 是根据 authenticate 即认证后得到的 AuthenticationInfo 信息来的,也就是说这些认证的信息是保存在 subject 中的,也就是下文中要提到的 subject 的 principals 和 authenticated 属性。
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
public AuthorizationAttributeSourceAdvisor() {
setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
}
关于 AuthorizationAttributeSourceAdvisor:
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {
private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);
private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
new Class[] {
RequiresPermissions.class, RequiresRoles.class,
RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
};
public boolean matches(Method method, Class targetClass) {
Method m = method;
if ( isAuthzAnnotationPresent(m) ) {
return true;
}
//The 'method' parameter could be from an interface that doesn't have the annotation.
//Check to see if the implementation has it.
if ( targetClass != null) {
try {
m = targetClass.getMethod(m.getName(), m.getParameterTypes());
return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);
} catch (NoSuchMethodException ignored) {
//default return value is false. If we can't find the method, then obviously
//there is no annotation, so just use the default return value.
}
}
return false;
}
private boolean isAuthzAnnotationPresent(Class<?> targetClazz) {
for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass);
if ( a != null ) {
return true;
}
}
return false;
}
private boolean isAuthzAnnotationPresent(Method method) {
for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
Annotation a = AnnotationUtils.findAnnotation(method, annClass);
if ( a != null ) {
return true;
}
}
return false;
}
这里简要的说以下几点:
关于 AopAllianceAnnotationsAuthorizingMethodInterceptor:
public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
List<AuthorizingAnnotationMethodInterceptor> interceptors =
new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
//use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
//raw JDK resolution process.
AnnotationResolver resolver = new SpringAnnotationResolver();
//we can re-use the same resolver instance - it does not retain state:
interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
interceptors.add(new UserAnnotationMethodInterceptor(resolver));
interceptors.add(new GuestAnnotationMethodInterceptor(resolver));
setMethodInterceptors(interceptors);
}
根据构造方法可以看出,是加了很多的拦截器。
public AuthenticatedAnnotationMethodInterceptor(AnnotationResolver resolver) {
super(new AuthenticatedAnnotationHandler(), resolver);
}
AuthenticatedAnnotationHandler:
public AuthenticatedAnnotationHandler() {
super(RequiresAuthentication.class);
}
public void assertAuthorized(Annotation a) throws UnauthenticatedException {
if (a instanceof RequiresAuthentication && !getSubject().isAuthenticated() ) {
throw new UnauthenticatedException( "The current Subject is not authenticated. Access denied." );
}
}
是通过 getSubject().isAuthenticated()来做校验的,也就是 subject 的 protected boolean authenticated 属性。
public PermissionAnnotationMethodInterceptor() {
super( new PermissionAnnotationHandler() );
}
public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) {
super( new PermissionAnnotationHandler(), resolver);
}
PermissionAnnotationHandler:
protected String[] getAnnotationValue(Annotation a) {
RequiresPermissions rpAnnotation = (RequiresPermissions) a;
return rpAnnotation.value();
}
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (!(a instanceof RequiresPermissions)) return;
RequiresPermissions rpAnnotation = (RequiresPermissions) a;
String[] perms = getAnnotationValue(a);
Subject subject = getSubject();
if (perms.length == 1) {
subject.checkPermission(perms[0]);
return;
}
if (Logical.AND.equals(rpAnnotation.logical())) {
getSubject().checkPermissions(perms);
return;
}
if (Logical.OR.equals(rpAnnotation.logical())) {
// Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
boolean hasAtLeastOnePermission = false;
for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true;
// Cause the exception if none of the role match, note that the exception message will be a bit misleading
if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]);
}
}
org.apache.shiro.subject.support.DelegatingSubject#checkPermission(java.lang.String):
public void checkPermission(String permission) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermission(getPrincipals(), permission);
}
org.apache.shiro.mgt.AuthorizingSecurityManager#checkPermission(org.apache.shiro.subject.PrincipalCollection, java.lang.String):
public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
this.authorizer.checkPermission(principals, permission);
}
org.apache.shiro.realm.AuthorizingRealm#checkPermission(org.apache.shiro.subject.PrincipalCollection, java.lang.String):
public void checkPermission(PrincipalCollection subjectIdentifier, String permission) throws AuthorizationException {
Permission p = getPermissionResolver().resolvePermission(permission);
checkPermission(subjectIdentifier, p);
}
public void checkPermission(PrincipalCollection principal, Permission permission) throws AuthorizationException {
AuthorizationInfo info = getAuthorizationInfo(principal);
checkPermission(permission, info);
}
org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
}
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
if (log.isTraceEnabled()) {
log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
}
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
} else {
log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
}
}
}
if (info == null) {
// Call template method if the info was not found in a cache
info = doGetAuthorizationInfo(principals);
// If the info is not null and the cache has been created, then cache the authorization info.
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info);
}
}
return info;
}
接下来就到了很熟悉的 doGetAuthorizationInfo 方法了,它是一个抽象方法,由用户自定义的 realm 来实现,我的实现为:
com.shimh.oauth.OAuthRealm#doGetAuthorizationInfo:
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String account = (String) principals.getPrimaryPrincipal();
User user = userService.getUserByAccount(account);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<String>();
//简单处理 只有admin一个角色
if (user.getAdmin()) {
roles.add(BaseConstant.ROLE_ADMIN);
}
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
这里需要调用 subject 的 principals 来获取用户信息。
public RoleAnnotationMethodInterceptor(AnnotationResolver resolver) {
super(new RoleAnnotationHandler(), resolver);
}
org.apache.shiro.authz.aop.RoleAnnotationHandler#RoleAnnotationHandler:
public RoleAnnotationHandler() {
super(RequiresRoles.class);
}
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (!(a instanceof RequiresRoles)) return;
RequiresRoles rrAnnotation = (RequiresRoles) a;
String[] roles = rrAnnotation.value();
if (roles.length == 1) {
getSubject().checkRole(roles[0]);
return;
}
if (Logical.AND.equals(rrAnnotation.logical())) {
getSubject().checkRoles(Arrays.asList(roles));
return;
}
if (Logical.OR.equals(rrAnnotation.logical())) {
// Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
boolean hasAtLeastOneRole = false;
for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;
// Cause the exception if none of the role match, note that the exception message will be a bit misleading
if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);
}
}
org.apache.shiro.subject.support.DelegatingSubject#checkRole:
public void checkRole(String role) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkRole(getPrincipals(), role);
}
org.apache.shiro.mgt.AuthorizingSecurityManager#checkRole:
public void checkRole(PrincipalCollection principals, String role) throws AuthorizationException {
this.authorizer.checkRole(principals, role);
}
org.apache.shiro.realm.AuthorizingRealm#checkRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String):
public void checkRole(PrincipalCollection principal, String role) throws AuthorizationException {
AuthorizationInfo info = getAuthorizationInfo(principal);
checkRole(role, info);
}
接下来调用的方法和上面的 PermissionAnnotationMethodInterceptor 相同,这里就不再赘述。最后也还是会调用自定义 Realm 的授权方法 doGetAuthorizationInfo。
ArticleController 的发布方法如下:
@PostMapping("/publish")
@RequiresAuthentication
@LogAnnotation(module = "文章", operation = "发布文章")
@AuthPassport(noNeed = true)
public Result saveArticle(@Validated @RequestBody Article article, @CurrentUser UserTo userTo) {
Integer articleId = articleService.publishArticle(article,userTo);
Result r = Result.success();
r.simple().put("articleId", articleId);
return r;
}
可见是加了 RequiresAuthentication 注解的,请求时会被拦截如下: org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor#invoke:
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
return super.invoke(mi);
}
然后调用 org.apache.shiro.authz.aop.AuthorizingMethodInterceptor#invoke:
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
assertAuthorized(methodInvocation);
return methodInvocation.proceed();
}
org.apache.shiro.authz.aop.AnnotationsAuthorizingMethodInterceptor#assertAuthorized:
protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
//default implementation just ensures no deny votes are cast:
Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
if (aamis != null && !aamis.isEmpty()) {
for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
if (aami.supports(methodInvocation)) {
aami.assertAuthorized(methodInvocation);
}
}
}
}
org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor#assertAuthorized:
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
try {
((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
}
catch(AuthorizationException ae) {
// Annotation handler doesn't know why it was called, so add the information here if possible.
// Don't wrap the exception here since we don't want to mask the specific exception, such as
// UnauthenticatedException etc.
if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod()));
throw ae;
}
}
org.apache.shiro.authz.aop.AuthenticatedAnnotationHandler#assertAuthorized:
public void assertAuthorized(Annotation a) throws UnauthenticatedException {
if (a instanceof RequiresAuthentication && !getSubject().isAuthenticated() ) {
throw new UnauthenticatedException( "The current Subject is not authenticated. Access denied." );
}
}
其中 org.apache.shiro.aop.AnnotationHandler#getSubject:
protected Subject getSubject() {
return SecurityUtils.getSubject();
}
关于这个方法的流程上面已经讲过,其实就是把与当前 IO 线程绑定的 subject 对象取出来的过程,如果开启了 session 会同步 session 中的信息(本文开头部分有讲过),如果没有开启 session,里面是没有任何权限和用户信息的。
这里补充一下从 session 中同步信息到 subject 中的流程: org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
org.apache.shiro.web.subject.WebSubject.Builder#buildWebSubject:
public WebSubject buildWebSubject() {
Subject subject = super.buildSubject();
if (!(subject instanceof WebSubject)) {
String msg = "Subject implementation returned from the SecurityManager was not a " +
WebSubject.class.getName() + " implementation. Please ensure a Web-enabled SecurityManager " +
"has been configured and made available to this builder.";
throw new IllegalStateException(msg);
}
return (WebSubject) subject;
}
org.apache.shiro.subject.Subject.Builder#buildSubject:
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
org.apache.shiro.mgt.DefaultSecurityManager#createSubject(org.apache.shiro.subject.SubjectContext):
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);
//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);
//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
也就是说如果关闭了 session,就需要自己想办法往这个 subject 中添加用户的权限信息。
加一个 filter 如下:
public class JwtAuthFilter extends AuthenticatingFilter {
/**
* 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(this.isLoginRequest(request, response))
return true;
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch(IllegalStateException e){ //not found any token
log.error("Not found any token");
}catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
//这里是存放在请求的header中的,也可以存放在cookie中
String jwtToken = getAuthzHeader(servletRequest);
if(StringUtils.isNotBlank(jwtToken)&&!JwtUtils.isTokenExpired(jwtToken))
return new JWTToken(jwtToken);
return null;
}
..........
executeLogin 中会调用 createToken 和 getSubject 方法,进行 subject.login 操作:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
其中 createToken 为自定义的 filter 中重写的方法,getSubject 方法如下:
protected Subject getSubject(ServletRequest request, ServletResponse response) {
return SecurityUtils.getSubject();
}
依然是我们很熟悉的那个 subject。
该方案引用自网络,个人认为理论上是可行的,并未自测,出自:https://www.jianshu.com/p/0b1131be7ace
自定义拦截器,然后调用 subject.login 来验证:
Cookie[] cookies = request.getCookies();
if (cookies != null) {
Optional<Cookie> cookieOptional = Arrays.stream(cookies).filter(cookie -> BaseConstant.AUTH.equals(cookie.getName())).findFirst();
if (cookieOptional.isPresent()) {
Cookie cookie = cookieOptional.get();
try {
Map<String, Object> paramMap = JwtUtil.decode(BaseConstant.SHIRO_KEY, cookie.getValue(), BaseConstant.SHIRO_SALT);
String userObj = (String)paramMap.get("user");
UserTo userTo = JSONObject.parseObject(userObj, UserTo.class);
Subject subject = SecurityUtils.getSubject();
//调用了login之后会校验OAuthRealm的supports方法,查看token类型
//注意,这里调用subject.login是为了给当前线程的subject进行认证操作,赋给principles和authorized属性值
StatelessToken token = new StatelessToken(userTo.getAccount(),cookie.getValue(),BaseConstant.SHIRO_SALT);
subject.login(token);
return Optional.of(userTo);
} catch (Exception e) {
log.error("cookie的值为:" + cookie.getValue() + "解密失败!", e);
}
}
}
return Optional.empty();
其中 OAuthRealm 的代码如下:
public class OAuthRealm extends AuthorizingRealm {
private static final Logger LOG = LoggerFactory.getLogger(OAuthRealm.class);
@Autowired
private UserService userService;
public OAuthRealm() {
this.setCredentialsMatcher(new JWTCredentialsMatcher());
}
@Override
public boolean supports(AuthenticationToken token) {
//仅支持StatelessToken类型的
return token instanceof StatelessToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String account = (String) principals.getPrimaryPrincipal();
User user = userService.getUserByAccount(account);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<String>();
//简单处理 只有admin一个角色
if (user.getAdmin()) {
roles.add(BaseConstant.ROLE_ADMIN);
}
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
StatelessToken statelessToken = (StatelessToken) token;
String username = (String) statelessToken.getPrincipal();
//密码,shiro会根据token的credentials进行加密然后与SimpleAuthenticationInfo的第二个参数进行比较
//String loginedToken = (String) statelessToken.getCredentials();
//通过校验
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username,
statelessToken.getCredentials(),
ByteSource.Util.bytes(statelessToken.getSalt()),
getName()
);
return authenticationInfo;
}
}
JWTCredentialsMatcher 代码如下:
public class JWTCredentialsMatcher implements CredentialsMatcher{
/**
* 这里只是简单实现下,可以使用jwt的verify方法来校验
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
if (info instanceof SimpleAuthenticationInfo){
SimpleAuthenticationInfo simpleAuthenticationInfo = (SimpleAuthenticationInfo) info;
PrincipalCollection principals = info.getPrincipals();
if (principals.isEmpty()){
return false;
}
if (token instanceof StatelessToken){
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
String tokenPrincipal = (String) token.getPrincipal();
String credentials = (String) info.getCredentials();
String credentials1 = (String) token.getCredentials();
String toHex = simpleAuthenticationInfo.getCredentialsSalt().toHex();
StatelessToken statelessToken = (StatelessToken) token;
String toHex1 = ByteSource.Util.bytes(statelessToken.getSalt()).toHex();
if (primaryPrincipal.equals(tokenPrincipal) && credentials.equals(credentials1) && toHex.equals(toHex1)){
return true;
}
return false;
}else {
return false;
}
}
return false;
}
}
然后就可以成功了,亲测有效。可以结合注解注入当前用户(之前的文章详细地讲解过,这里不再赘述)
亲测有效哦