1、环境
SpringBoot 2.2.4.RELEASE
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
jdk 1.8
shiro 1.4.0
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2、配置FilterConfig,拦截请求。
- 配置类,添加
FilterRegistrationBean
的bean,拦截客户端请求 registration.setFilter()
这里设置的DelegatingFilterProxy
与后面的Filter Bean必须一样。- 这里的配置与之前在Spring的时候在web.xml配置一样,只不过这里使用了配置类的方法
FilterRegistrationBean
可以返回不同类型的Filter,public class FilterRegistrationBean extends AbstractFilterRegistrationBean {
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
//该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
registration.addInitParameter("targetFilterLifecycle", "true");
registration.setEnabled(true);
registration.setOrder(Integer.MAX_VALUE - 1);
registration.addUrlPatterns("/*");
return registration;
}
- 这里给上在
web.xml
上的类似配置
<!-- Shiro Filter is defined in the spring application context: -->
<!--
1. 配置 Shiro 的 shiroFilter.
2. DelegatingFilterProxy 实际上是 Filter 的一个代理对象. 默认情况下, Spring 会到 IOC 容器中查找和
<filter-name> 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id.
-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、配置ShiroConfig 类
- ShiroConfig类里面一般需要四个bean:
SecurityManager
,shiroFilter
,lifecycleBeanPostProcessor
,authorizationAttributeSourceAdvisor
。
3.1、配置一个SecurityManager
- 在项目中一般会自定义权限Realm,这里我使用的是
oAuth2Realm
,后面会讲。
@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
// 配置securityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm);
// securityManager.setRealms(); // 可以设置多个Realm
securityManager.setRememberMeManager(null);
// 设置Authenticator,再由Authenticator设置策略
/**
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
*/
// securityManager.setAuthenticator(modularRealmAuthenticator);
// securityManager.setCacheManager();
return securityManager;
}
//
- 这里就类似你之前在
applicationContext.xml
配置的类似
<-- 1. 配置 SecurityManager!
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="authenticator" ref="authenticator"></property>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
<!--
3. 配置 Realm
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="com.atguigu.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
- 最后判断Realm的额外说明
// 这里给securityManager设置Realm,其实会赋给authenticator,再由他的实现类ModularRealmAuthenticator的doAuthenticate方法里面调用
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
// 在AuthenticatingSecurityManager里面赋值
protected void afterRealmsSet() {
super.afterRealmsSet();
if (this.authenticator instanceof ModularRealmAuthenticator) {
((ModularRealmAuthenticator)this.authenticator).setRealms(this.getRealms());
}
}
// 会将赋给securityManager的Realm赋给authenticator(ModularRealmAuthenticator)
3.2、配置shiroFilter
@Bean("shiroFilter")
bean的名字要和FilterConfig
里面的名字一样
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//oauth过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
shiroFilter.setFilters(filters);
/**
* 配置哪些资源需要受保护,以及访问这些页面需要的权限
* 这里使用的是LinkedHashMap,所以是有顺序的,所以一般吧静态资源或者不需要验证的放在前面
* 否则的话也是会被拦截。
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon"); // anon说明可以匿名访问
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/aaa.txt", "anon");
filterMap.put("/**", "oauth2"); // /**表明必须通过该oauth2定义的认证才可以通过
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
- 这里就类似你之前在
applicationContext.xml
配置的类似
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
- 上面的
OAuth2Filter()
。 - 这个类的主要作用就是在获取客户端传来的token,方便后面验证,并且重写相关方法,加入自己的逻辑验证。
executeLogin(request, response);
验证入口,后面会说到验证流程。
public class OAuth2Filter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
return null;
}
return new OAuth2Token(token);
/**
OAuth2Token 类
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token){
this.token = token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
*/
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
httpResponse.getWriter().print(json);
return false;
}
// 验证入口
return executeLogin(request, response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
String json = new Gson().toJson(r);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest){
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)){
token = httpRequest.getParameter("token");
}
return token;
}
}
3.3、配置lifecycleBeanPostProcessor
/**
* lifecycleBeanPostProcessor 可以自动的来调用配置在Spring IOC
* 容器中shiro bean的生命周期方法
* @return
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
- 这里就类似你之前在
applicationContext.xml
配置的类似
<!--
4. 配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
3.4、配置authorizationAttributeSourceAdvisor
/**
* AuthorizationAttributeSourceAdvisor
* 弃用IOC容器中使用shiro注解,但是必须在配置了LifecycleBeanPostProcessor 之后才可以使用
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
- 这里就类似你之前在
applicationContext.xml
配置的类似
<!--
5. 启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用.
-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
4、验证过程
- 上面我还有一个没有说,那就是自定义
oAuth2Realm
类。 - 继承AuthorizingRealm 或者AuthorizingRealm 的父类,实现两个抽象类
doGetAuthenticationInfo
是登录认证的时候用的。doGetAuthorizationInfo
是验证权限的时候用的
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private ShiroService shiroService;
@Override
public boolean supports(AuthenticationToken token) {
// 在 AuthenticatingRealm 里面的方法
return token instanceof OAuth2Token; // 是属于AuthenticationToken 这个类型的
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/**
* getPrimaryPrincipal()在多Realm的时候是按照先后顺序的获取到principals 结构是 LinkedHashSet()
* SimplePrincipalCollection类 --》 public Object getPrimaryPrincipal() {
* return this.isEmpty() ? null : this.iterator().next();
* }
* iterator() --》 public Iterator iterator() {
* return this.asSet().iterator();
* }
*
* asSet() --》
* public Set asSet() {
* if (this.realmPrincipals != null && !this.realmPrincipals.isEmpty()) {
* Set aggregated = new LinkedHashSet();
* Collection<Set> values = this.realmPrincipals.values();
* Iterator var3 = values.iterator();
*
* .....
*/
SysUserEntity user = (SysUserEntity) principals.getPrimaryPrincipal();
Long userId = user.getUserId();
// 用户权限列表
// 获取到该用户的全部perms(如sys:schedule:save,sys:schedule:update等)
Set<String> permsSet = shiroService.getUserPermissions(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
/**
* 需要一个Set集合
* public void setStringPermissions(Set<String> stringPermissions) {
* this.stringPermissions = stringPermissions;
* }
*/
/**
* 权限授权连接器,验证用户是都拥有所有权限;属性和roles一样 示例:"/user/**"= perms["user:create"]
*/
info.setStringPermissions(permsSet);
// info.setRoles(); // 角色授权拦截器,验证用户是否拥有所有的角色,主要属性
// loginUrl:登录地址(/login.jsp)unauthorizedUrl:未授权后重定向的地址;示例"/admin/**" = roles[admin]
// info.a
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
// 根据accessToken,查询用户信息
// 返回自定义生成的token(不同用户在登陆的时候会生成一个token并且会保存在数据库,并将token和过期时间返回给页面)
SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
// token失效
// tokenEntity.getExpireTime().getTime()过期时间
if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
throw new IncorrectCredentialsException("token失效,请重新登录");
}
//查询用户信息
SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
//账号锁定
if(user.getStatus() == 0){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
// TODO user, accessToken, getName()user需要认证的用户? getName()当前类的名字?
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
/**
* SimpleCredentialsMatcher(在这个方法中进行token的比对)
* public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
* Object tokenCredentials = this.getCredentials(token);
* Object accountCredentials = this.getCredentials(info);
* return this.equals(tokenCredentials, accountCredentials);
* }
*/
return info;
}
}
4.1、首先说明doGetAuthenticationInfo
-
这个方法会在用户登录的时候调用,这里我先说明一下,我上面所用的例子是比对token,我这里简单的说明一下我这个token的由来。
-
我会在登录的时候验证用户存在并返回成功信息给客户端之前把前端传来的token 保存在数据库中。这也是我上面doGetAuthenticationInfo方法中会把前端传来的token以及根据token查询出来的用户信息传给SimpleAuthenticationInfo的原因,你也可以根据自己的需求比对其他来验证
-
数据库中类似
-
根据最后
SimpleAuthenticationInfo(user, accessToken, getName());
传入的信息看,我们可以在SimpleAuthenticationInfo里面的getCredentials
上面打断点调试,寻找它在什么地方比对的token
-
在客户端登录,在断点处停,查看。
-
最后我们发现在
SimpleCredetialsMatcher
的类里面进行比对
-
而在执行
SimpleCredetialsMatcher
这个类的方法之前,我们可以看到获取到的就是我们自定义的Realm
-
至于怎么获取的,自己可以根据断点的执行点一个个往前看,不难我发现,最终是从我一开始说 验证入口开始的。我这里就给出一个大概流程。代码只截取部分
// 1、在前面OAuth2Filter里面就会有一个执行入口,原因就是你在ShiroConfig里面配置了Filter是你自定义的OAuth2Filter,所以会经过该Filter
return executeLogin(request, response); // 这就是你的入口
// 2、它会调用AuthenticatingFilter里面的executeLogin
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 2.1、方法里面继续调用subject.login(token);subject.login(token);
Subject subject = this.getSubject(request, response);
subject.login(token);
// 3、再调用DelegatingSubject里面的login
public void login(AuthenticationToken token) throws AuthenticationException {
// 3.1 方法里面调用
Subject subject = this.securityManager.login(this, token);
// 4、再调用DefaultSecurityManager里面的login
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
// 4.1、方法里面调用
info = this.authenticate(token);
// 5、调用AuthenticatingSecurityManager里面authenticate
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
// 6、调用AbstractAuthenticator里面的authenticate
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
info = this.doAuthenticate(token);
// 7、调用ModularRealmAuthenticator里面的doAuthenticate,这里就是遍历获取我们的Realm的
// 前面有讲过为什么最后会调用authenticator来获取Realm,而我们是赋给SecurityManager的
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
// 8、调用 ModularRealmAuthenticator的doSingleRealmAuthentication方法
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
AuthenticationInfo info = realm.getAuthenticationInfo(token);
// 9、调用AuthenticatingRealm的getAuthenticationInfo,获取到我们的Info,然后调用
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
this.assertCredentialsMatch(token, info);
// 10、调用AuthenticatingRealm的assertCredentialsMatch方法,在里面比对
if (!cm.doCredentialsMatch(token, info)) {
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
// 11、最后就是SimpleCredentialsMatcher里面的doCredentialsMatch方法返回
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
- 经过这些流程之后就会认证是否可以登录成功。
- 至于前面我们是如何获取filterMap 我们定义的这些,你可以在
setFilterChainDefinitionMap
上面按住ctrl+鼠标左键进去。
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon"); // anon说明可以匿名访问
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/aaa.txt", "anon");
filterMap.put("/**", "oauth2"); // /**表明必须通过该oauth2定义的认证才可以通过
shiroFilter.setFilterChainDefinitionMap(filterMap);
- 然后在上面加上断点,按照断点查看就会知道怎么获取到得了
4.1、然后说明doGetAuthorizationInfo
- 这个方法会在检查用户是否有权限的时候调用的。
- 而我在自定义Realm里面重写了用户的权限Set集合。
- 具体设计是用户有对应的角色,而角色里面关联了对应的权限集合,类似表如下
- 而前面我在自定义OAuth2Realm里面把自己需要的验证的放入到
SimpleAuthorizationInfo
里面。前面已帖代码,这里是为了讲述。
Set<String> permsSet = shiroService.getUserPermissions(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
/**
* 需要一个Set集合
* public void setStringPermissions(Set<String> stringPermissions) {
* this.stringPermissions = stringPermissions;
* }
*/
/**
* 权限授权连接器,验证用户是都拥有所有权限;属性和roles一样 示例:"/user/**"= perms["user:create"]
*/
info.setStringPermissions(permsSet);
- 想要知道怎么获取,也是一样的套路,进入
setStringPermissions
里面、加断点。
- 这里就贴出它的主要方法
// 验证用户拥有的权限
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = this.getPermissions(info);
if (perms != null && !perms.isEmpty()) {
Iterator var4 = perms.iterator();
while(var4.hasNext()) {
Permission perm = (Permission)var4.next();
if (perm.implies(permission)) {
return true;
}
}
}
- 访问需要验证权限的接口。类似于
@RequiresPermissions("sys:config:list")
@GetMapping("/list")
@RequiresPermissions("sys:config:list")
public R list(@RequestParam Map<String, Object> params){
- 可以使用
@RequiresPermissions
注解因为在前面ShiroConfig配置类配置了authorizationAttributeSourceAdvisor。
5、总结
- SpringBoot中使用Shiro做权限以及认证的需求
- 1:需要一个FilterConfig类 --》FilterRegistrationBean 的bean 来配置拦截客户端请求。
- 2:需要一个ShiroConfig配置类来配置SecurityManager、ShiroFilter、LfecycleBeanPostProcessor、AuthorizationAttributeSourceAdvisor等bean信息。
- 3:给securityManager(new DefaultWebSecurityManager())赋值一个自定义oAuth2Realm。
- 4:给ShiroFilter赋值一个自定义OAuth2Filter,以及设置一个FilterChainDefinitionMap(类似于filterMap.put("/druid/**", “anon”);)
- 5:给AuthorizationAttributeSourceAdvisor赋值一个securityManager
- 6:给对应接口添加权限注解。