Shrio使用Jwt达到前后端分离

概述

前后端分离之后,因为HTTP本身是无状态的,Session就没法用了。项目采用jwt的方案后,请求的主要流程如下:用户登录成功之后,服务端会创建一个jwt的token(jwt的这个token中记录了当前的操作账号),并将这个token返回给前端,前端每次请求服务端的数据时,都会将令牌放入Header或者Parameter中,服务端接收到请求后,会先被拦截器拦截,token检验的拦截器会获取请求中的token,然后会检验token的有效性,拦截器都检验成功后,请求会成功到达实际的业务流程中,执行业务逻辑返回给前端数据。在这个过程中,主要涉及到Shiro的拦截器链,Jwt的token管理,多Realm配置等。

Shiro的Filter链

Shiro的认证和授权都离不开Filter,因此需要对Shiro的Filter的运行流程很清楚,才能自定义Filter来满足企业的实际需要。另外Shiro的Filter虽然原理都和Servlet的Filter相似,甚至都最终继承相同的接口,但是实际还是有些差别。Shiro中的Filter主要是在ShiroFilter内,对指定匹配的URL进行拦截处理,它有自己的Filter链;而Servlet的Filter和ShiroFilter是同一个级别的,即先走Shiro自己的Filter体系,然后才会委托给Servlet容器的FilterChain进行Servlet容器级别的Filter链执行

分析Shiro的默认Filter

在Shiro和Spring Boot整合过程中,需要配置ShiroFilterFactoryBean,该类是ShiroFilter的工厂类,并继承FactoryBean接口。可以从该接口的方法来分析。该接口getObject获取一个实例,按照逻辑,发现调用createFilterChainManager,并创建默认的Filter(按照命名猜测Map<String, Filter> defaultFilters = manager.getFilters())。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
    private Map<String, Filter> filters;
 
    private Map<String, String> filterChainDefinitionMap; 
 
    /**
     *
     * 该工厂类生产的产品类
     */
    public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();
        }
        return instance;
    }
 
    protected FilterChainManager createFilterChainManager() {
        //创建默认Filter
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        Map<String, Filter> defaultFilters = manager.getFilters();
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }
 
        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                manager.addFilter(name, filter, false);
            }
        }
 
        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                manager.createChain(url, chainDefinition);
            }
        }
 
        return manager;
    }
 
    protected AbstractShiroFilter createInstance() throws Exception {
 
        log.debug("Creating Shiro Filter instance.");
 
        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }
 
        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }
        //创建FilterChainManager
        FilterChainManager manager = createFilterChainManager();
 
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
 
        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }
 
   ...
}

private Map<String, String> filterChainDefinitionMap;

/**
*
* 该工厂类生产的产品类
*/
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}

protected FilterChainManager createFilterChainManager() {
//创建默认Filter
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters();
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}

Map<String, Filter> filters = getFilters();
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry<String, Filter> entry : filters.entrySet()) {
String name = entry.getKey();
Filter filter = entry.getValue();
applyGlobalPropertiesIfNecessary(filter);
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
manager.addFilter(name, filter, false);
}
}

Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
manager.createChain(url, chainDefinition);
}
}

return manager;
}

protected AbstractShiroFilter createInstance() throws Exception {

log.debug("Creating Shiro Filter instance.");

SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}

if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
//创建FilterChainManager
FilterChainManager manager = createFilterChainManager();

PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);

return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}

...
}

DefaultFilterChainManageraddDefaultFilters来添加默认的Filter,DefaultFilter为一系列默认Filter的枚举类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DefaultFilterChainManager implements FilterChainManager {
 
    public Map<String, Filter> getFilters() {
        return filters;
    }
 
    protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
        Filter existing = getFilter(name);
        if (existing == null || overwrite) {
            if (filter instanceof Nameable) {
                ((Nameable) filter).setName(name);
            }
            if (init) {
                initFilter(filter);
            }
            this.filters.put(name, filter);
        }
    }
 
     /**
     *
     * 创建默认的Filter
     */
    protected void addDefaultFilters(boolean init) {
        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
            addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
        }
    }
    ...
}

public Map<String, Filter> getFilters() {
return filters;
}

protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
Filter existing = getFilter(name);
if (existing == null || overwrite) {
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
if (init) {
initFilter(filter);
}
this.filters.put(name, filter);
}
}

/**
*
* 创建默认的Filter
*/
protected void addDefaultFilters(boolean init) {
for (DefaultFilter defaultFilter : DefaultFilter.values()) {
addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
}
}
...
}

从这个枚举类中可以看到之前添加的共有11个默认Filter,它们的名字分别是anon,authc,authcBaisc等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public enum DefaultFilter {
 
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
 
    private final Class<? extends Filter> filterClass;
 
    private DefaultFilter(Class<? extends Filter> filterClass) {
        this.filterClass = filterClass;
    }
 
    public Filter newInstance() {
        return (Filter) ClassUtils.newInstance(this.filterClass);
    }
 
    public Class<? extends Filter> getFilterClass() {
        return this.filterClass;
    }
    ...
}

anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);

private final Class<? extends Filter> filterClass;

private DefaultFilter(Class<? extends Filter> filterClass) {
this.filterClass = filterClass;
}

public Filter newInstance() {
return (Filter) ClassUtils.newInstance(this.filterClass);
}

public Class<? extends Filter> getFilterClass() {
return this.filterClass;
}
...
}

Filter的继承体系分析

  • NameableFilter给Filter起个名字,如果没有设置,默认名字就是FilterName。
  • OncePerRequestFilter用于防止多次执行Filter;也就是说一次请求只会走一次拦截器链;另外提供 enabled 属性,表示是否开启该拦截器实例,默认 enabled=true 表示开启,如果不想让某个拦截器工作,可以设置为 false 即可。
  • AdviceFilter提供了AOP风格的支持。preHandler:在拦截器链执行之前执行,如果返回true则继续拦截器链;否则中断后续的拦截器链的执行直接返回;可以进行预处理(如身份验证、授权等行为)。postHandle:在拦截器链执行完成后执行,后置处理(如记录执行时间之类的)。afterCompletion:类似于AOP中的后置最终增强;即不管有没有异常都会执行,可以进行清理资源(如接触 Subject 与线程的绑定之类的)。
  • PathMatchingFilter内置了pathMatcher的实例,方便对请求路径匹配功能及拦截器参数解析的功能,如下所示,对匹配的路径执行isFilterChainContinued的逻辑,如果都没配到,则直接交给拦截器链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
 
    if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
        if (log.isTraceEnabled()) {
            log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
        }
        return true;
    }
 
    for (String path : this.appliedPaths.keySet()) {
        //对匹配路径进行处理
        if (pathsMatch(path, request)) {
            log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
            Object config = this.appliedPaths.get(path);
            return isFilterChainContinued(request, response, path, config);
        }
    }
 
    return true;
}

if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately.");
}
return true;
}

for (String path : this.appliedPaths.keySet()) {
//对匹配路径进行处理
if (pathsMatch(path, request)) {
log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path);
Object config = this.appliedPaths.get(path);
return isFilterChainContinued(request, response, path, config);
}
}

return true;
}

  • AccessControlFilter提供了访问控制的基础功能,isAccessAllowed访问通过,则交给拦截器链,不通过则执行onAccessDenied来确定交给拦截器还是自己处理
1
2
3
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }
  • AuthenticationFilter认证Filter的基类,一般在isAccessAllowed中执行认证逻辑,另外该Filter提供登录成功后跳转的功能
1
2
3
4
5
6
7
8
9
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object      mappedValue) {
    Subject subject = getSubject(request, response);
    return subject.isAuthenticated();
}
 
 
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws        Exception {
    WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}

protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}

  • AuthenticatingFilter是AuthenticationFilter的子类,提供了executeLogin通用逻辑,通常由子类来实现protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response)该方法,然后执行subject.login(token)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public abstract class AuthenticatingFilter extends AuthenticationFilter {
    public static final String PERMISSIVE = "permissive";
 
    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);
        }
    }
 
    protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;
 
    protected AuthenticationToken createToken(String username, String password,
                                              ServletRequest request, ServletResponse response) {
        boolean rememberMe = isRememberMe(request);
        String host = getHost(request);
        return createToken(username, password, rememberMe, host);
    }
 
    protected AuthenticationToken createToken(String username, String password,
                                              boolean rememberMe, String host) {
        return new UsernamePasswordToken(username, password, rememberMe, host);
    }
 
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }
 
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        return false;
    }
 
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return super.isAccessAllowed(request, response, mappedValue) ||
            (!isLoginRequest(request, response) && isPermissive(mappedValue));
    }
    ...
}

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);
}
}

protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;

protected AuthenticationToken createToken(String username, String password,
ServletRequest request, ServletResponse response) {
boolean rememberMe = isRememberMe(request);
String host = getHost(request);
return createToken(username, password, rememberMe, host);
}

protected AuthenticationToken createToken(String username, String password,
boolean rememberMe, String host) {
return new UsernamePasswordToken(username, password, rememberMe, host);
}

protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request, ServletResponse response) throws Exception {
return true;
}

protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
ServletRequest request, ServletResponse response) {
return false;
}

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
...
}

在Shiro中添加自定义的Filter

从上面源码分析,知道了Shiro会提供11个默认的Filter,也是按照拦截器模式交由FilterChainManager来管理Filter,并最终返回SpringShiroFilter。所以添加自定义的Filter,主要有三步。

  • 实现自己的Filter

如下实现了自己的JwtFilter,主要逻辑可以参考FormAuthenticationFilter。JwtFilter主要是对前端的Api进行校验,检验失败,则抛出异常信息,不给拦截器链处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Slf4j
public class JwtFilter extends AuthenticatingFilter {   
    private static final String TOKEN_NAME = "token";
 
    /**
     * 创建令牌
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        final String token = getToken((HttpServletRequest) servletRequest);     
        if(StringUtils.isEmpty(token)) {
            return null;
        }       
        return new JwtToken(token);     
    }
 
    /**
     * 获取令牌
     * @param httpServletRequest
     * @return
     */
    private String getToken(HttpServletRequest httpServletRequest) {
        String token = httpServletRequest.getHeader(TOKEN_NAME);
        if(StringUtils.isEmpty(token)) {
            token = httpServletRequest.getParameter(TOKEN_NAME);
        };
        if(StringUtils.isEmpty(token)) {
            Cookie[] cookies = httpServletRequest.getCookies();
            if(ArrayUtils.isNotEmpty(cookies)) {
                for(Cookie cookie :cookies) {
                    if(TOKEN_NAME.equals(cookie.getName())) {
                        token = cookie.getValue();
                        break;
                    }
                }
            }
        };  
        return token;
    }
 
    /**
     * 未通过处理
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        return executeLogin(servletRequest, servletResponse);
    }
 
    /**
     * 登录失败执行方法
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
            ServletResponse response) {
        response.setContentType("text/html;charset=UTF-8");
        try(OutputStream outputStream = response.getOutputStream()){
            outputStream.write(e.getMessage().getBytes(SystemConsts.CHARSET));
            outputStream.flush();           
        } catch (IOException e1) {
            e1.printStackTrace();
        }   
        return false;
    }
    ...
}

/**
* 创建令牌
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
final String token = getToken((HttpServletRequest) servletRequest);
if(StringUtils.isEmpty(token)) {
return null;
}
return new JwtToken(token);
}

/**
* 获取令牌
* @param httpServletRequest
* @return
*/
private String getToken(HttpServletRequest httpServletRequest) {
String token = httpServletRequest.getHeader(TOKEN_NAME);
if(StringUtils.isEmpty(token)) {
token = httpServletRequest.getParameter(TOKEN_NAME);
};
if(StringUtils.isEmpty(token)) {
Cookie[] cookies = httpServletRequest.getCookies();
if(ArrayUtils.isNotEmpty(cookies)) {
for(Cookie cookie :cookies) {
if(TOKEN_NAME.equals(cookie.getName())) {
token = cookie.getValue();
break;
}
}
}
};
return token;
}

/**
* 未通过处理
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
return executeLogin(servletRequest, servletResponse);
}

/**
* 登录失败执行方法
* @param token
* @param e
* @param request
* @param response
* @return
*/
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
ServletResponse response) {
response.setContentType("text/html;charset=UTF-8");
try(OutputStream outputStream = response.getOutputStream()){
outputStream.write(e.getMessage().getBytes(SystemConsts.CHARSET));
outputStream.flush();
} catch (IOException e1) {
e1.printStackTrace();
}
return false;
}
...
}

  • 将Filter添加到Shiro中

将自定义的Filter添加到Shiro,并要指定的匹配路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Autowired          org.apache.shiro.mgt.SecurityManager securityManager, @Autowired JwtFilter jwtFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
 
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", jwtFilter);
        shiroFilterFactoryBean.setFilters(filterMap);
 
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/**", "jwt"); 
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    ...
        return shiroFilterFactoryBean;
    }

Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", jwtFilter);
shiroFilterFactoryBean.setFilters(filterMap);

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
...
return shiroFilterFactoryBean;
}

注意:SpringBoot自动帮我们注册了我们的Filter(Filter是注册到整个Filter链,而不是Shiro的Filter链),但是在Shiro中,我们需要自己实现注册,但是又需要Filter实例存在于Spring容器中,以便能使用其他众多服务(自动注入其他组件……)。所以需要取消Spring Boot的自动注入Filter。可以采用如下方式:

1
2
3
4
5
6
@Bean
public FilterRegistrationBean registration(@Qualifier("devCryptoFilter") DevCryptoFilter filter){
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

Jwt整合

使用Jwt需要我们提供对token的创建,校验和获取token中信息的方法。网上有很多,可以借鉴,而且token中也可以存一些其他数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class JwtUtil {
 
    /**
     * 检验token
     * @return boolean
     */
    public static boolean verify(String token, String username) {
        ...
    }
 
    /**
     * 获得token中的属性
     * @return token中包含的属性
     */
    public static String getValue(String token, String key) {
        ...
    }
 
    /**
     * 生成token签名EXPIRE_TIME 分钟后过期
     * 
     * @param username
     *            用户名
     * @return 加密的token
     */
    public static String createJWT(String userId) {
        ...
    }
}

/**
* 检验token
* @return boolean
*/
public static boolean verify(String token, String username) {
...
}

/**
* 获得token中的属性
* @return token中包含的属性
*/
public static String getValue(String token, String key) {
...
}

/**
* 生成token签名EXPIRE_TIME 分钟后过期
*
* @param username
* 用户名
* @return 加密的token
*/
public static String createJWT(String userId) {
...
}
}

多Realm配置

用户密码认证和Jwt的认证需要不同的两个Realm,多Realm需要处理不同的Realm,获取到指定Realm的AuthenticationToken的数据模型。

  • 实现ModularRealmAuthenticator的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
 
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) 
            throws AuthenticationException {
        assertRealmsConfigured();
 
        List<Realm> realms = this.getRealms()
                .stream()
                .filter(realm -> {
                    return realm.supports(authenticationToken);
                })
                .collect(Collectors.toList());
 
        return realms.size() == 1 ? this.doSingleRealmAuthentication(realms.get(0), authenticationToken) : 
            this.doMultiRealmAuthentication(realms, authenticationToken);
    }
}

@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
assertRealmsConfigured();

List<Realm> realms = this.getRealms()
.stream()
.filter(realm -> {
return realm.supports(authenticationToken);
})
.collect(Collectors.toList());

return realms.size() == 1 ? this.doSingleRealmAuthentication(realms.get(0), authenticationToken) :
this.doMultiRealmAuthentication(realms, authenticationToken);
}
}

  • AuthenticatingRealm中实现getAuthenticationTokenClass方法
1
2
3
public Class getAuthenticationTokenClass() {
    return JwtToken.class;
}
  • 在SecurityManager中配置
1
2
3
4
5
6
7
@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager defaultWebSecurityManager(@Autowired UserRealm      userRealm,  @Autowired TokenRealm tokenValidateRealm) {
    securityManager.setAuthenticator(multiRealmAuthenticator());
    securityManager.setRealms(Arrays.asList(userRealm, tokenValidateRealm));
    ...
    return securityManager;
}

整合Swagger

添加Swagger依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
 
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

添加Swagger的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class Swagger2Config {
 
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("XXX"))
                .paths(PathSelectors.any())
                .build();
    }
 
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("XXX")
                .description("经供参考")
                .version("1.0")
                .build();
    }
}

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("XXX"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("XXX")
.description("经供参考")
.version("1.0")
.build();
}
}

总结

在整个过程中,遇到的坑就是在Spring boot中Filter的自动注入,中间考虑有不使用注入的方式解决,即直接使用new JwtFilter()的方式,虽然也能解决问题,但是不是很完美,最终还是在网上找到解决方案。对Shiro的Filter链的执行过程加强了理解,能够使用自定的Filter解决实际问题。还有一个后续的问题,退出登录时的Jwt的token处理,它本身不能像Session一样,退出就清除,理论上只要没过期,就一直存在。可以考虑使用缓存,退出时清除即可,然后在校验时,先从缓存获取进行判断。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

关注我们