互联网开发文档

互联网开发文档


Ruoyi框架学习笔记-Spring Security

<p>[TOC]</p> <h1>1. 简介</h1> <h1>2. 认证流程</h1> <p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=6a84b7aa8143afc60c4fcb74fe802fba&amp;amp;file=file.png" alt="" /></p> <ol> <li> <p>用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。</p> </li> <li> <p>然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证</p> </li> <li> <p>认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。</p> </li> <li>SecurityContextHolder 安全上下文容器将第3步填充了信息的Authentication ,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List&lt;AuthenticationProvider&gt; 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。</li> </ol> <h1>3. Authentication、SecurityContext和SecurityContextHolder</h1> <h2>3.0 认证登录触发调用</h2> <pre><code class="language-java">authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));</code></pre> <p>ruoyi完整登录逻辑:</p> <pre><code class="language-java">/** * 登录验证(每次登录都生成了新的Jwt令牌,有效期30分钟) * AuthenticationManager.authenticate() * -&amp;gt;委托认证DaoAuthenticationProvider.authenticate() * -&amp;gt;获取用户信息UserDetailsService.loadUserByUsername() * -&amp;gt;DaoAuthenticationProvider通过PasswordEncoder对比UserDetails中的密码与Authentication中的密码是否一致 * -&amp;gt;DaoAuthenticationProvider填充Authentication,如权限信息 * * @param username 用户名 * @param password 密码 * @param code 验证码 * @param uuid 唯一标识 * @return 结果:令牌 */ public String login(String username, String password, String code, String uuid) { boolean captchaOnOff = configService.selectCaptchaOnOff(); // 1.验证码开关 if (captchaOnOff) { //验证码开关True:校验验证码 validateCaptcha(username, code, uuid); } // 2.用户验证 Authentication authentication = null; try { // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername //会对查询到的UserDetails实现类(LoginUser)对象的isAccountNonExpired等方法进行验证 //会对用户传入的密码和数据库保存密码进行匹配验证 // authentication.getPrincipal()可以获取到UserDetails的实现类实例,需要加强制转换 authentication = authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (Exception e) { if (e instanceof BadCredentialsException) {//BadCredentialsException:密码不匹配异常 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message(&amp;quot;user.password.not.match&amp;quot;))); throw new UserPasswordNotMatchException(); } else { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); throw new ServiceException(e.getMessage()); } } //3.记录用户登录日志(sys_logininfor表) AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message(&amp;quot;user.login.success&amp;quot;))); //4.更新用户表的登录信息 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); //更新登录用户的基本信息到sys_user表 recordLoginInfo(loginUser.getUserId()); //5.生成Jwt令牌(令牌可以获取缓存中的用户信息) return tokenService.createToken(loginUser); }</code></pre> <h2>3.1 Authentication</h2> <p>authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,是一个接口,UsernamePasswordAuthenticationToken就是Authentication的一个实现类,一般来讲你可以理解为authentication就是一组用户名密码信息。</p> <pre><code class="language-java">public interface Authentication extends Principal, Serializable { Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; }</code></pre> <ol> <li>Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于java.security包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。</li> <li>getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。</li> <li>getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。</li> <li>getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。</li> <li><strong>getPrincipal()</strong>,身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。未认证的情况下获取到的是用户名。</li> </ol> <h2>3.2 SecurityContext</h2> <p>安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext的接口定义如下:</p> <pre><code class="language-java">public interface SecurityContext extends Serializable { /** * 获取当前已认证的主体或认证请求令牌 */ Authentication getAuthentication(); /** * 更改当前已认证的主体,或删除认证信息 */ void setAuthentication(Authentication authentication); }</code></pre> <h2>3.3 SecurityContextHolder</h2> <p>安全上线文容器:在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 SecurityContextHolder 所持有的SecurityContext。</p> <pre><code class="language-java">/** * 获取Authentication */ public static Authentication getAuthentication() { return SecurityContextHolder.getContext().getAuthentication(); }</code></pre> <h1>4. AuthenticationManager和AuthenticationProvider</h1> <h2>4.1 AuthenticationManager</h2> <p>AuthenticationManager是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:</p> <pre><code class="language-java">public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }</code></pre> <p>AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。</p> <h2>4.2 AuthenticationProvider</h2> <p>通过前面的Spring Security认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider完成认证工作。AuthenticationProvider是一个接口,定义如下:</p> <pre><code class="language-java">public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class&amp;lt;?&amp;gt; var1); }</code></pre> <p>authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户所提交的用户、密码等。而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。 Spring Security中维护着一个List&lt;AuthenticationProvider&gt; 列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用AuthenticationProvider2等等这样的例子很多。 每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider来处理它?我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码:</p> <pre><code class="language-java">public boolean supports(Class&amp;lt;?&amp;gt; authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); }</code></pre> <p>也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。</p> <h1>5. UserDetails和UserDetailsService的实现:LoginUser和UserDetailsServiceImpl</h1> <h2>5.1 UserDetails和其实现LoginUser</h2> <p>UserDetails存储的就是用户信息,其定义如下:</p> <pre><code class="language-java">public interface UserDetails extends Serializable { Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities(); //获取用户权限 String getPassword(); //获取密码 String getUsername(); //获取用户名 boolean isAccountNonExpired(); //账户是否过期 boolean isAccountNonLocked(); //账户是否被锁定 boolean isCredentialsNonExpired(); //密码是否过期 boolean isEnabled(); //账户是否可用 }</code></pre> <p>它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider认证之后被填充的。</p> <p>实现类LoginUser定义:</p> <pre><code class="language-java">/** * 登录用户身份权限 * 1.实现了UserDetails * 2.封装了用户信息:SysUser * 3.用户权限集合:Set&amp;lt;String&amp;gt; * * @author ruoyi */ public class LoginUser implements UserDetails { private static final long serialVersionUID = 1L; /** * 用户ID */ private Long userId; /** * 部门ID */ private Long deptId; /** * 用户唯一标识 */ private String token; /** * 登录时间 */ private Long loginTime; /** * 过期时间 */ private Long expireTime; /** * 登录IP地址 */ private String ipaddr; /** * 登录地点 */ private String loginLocation; /** * 浏览器类型 */ private String browser; /** * 操作系统 */ private String os; /** * 权限列表 */ private Set&amp;lt;String&amp;gt; permissions; /** * 用户信息 */ private SysUser user; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getDeptId() { return deptId; } public void setDeptId(Long deptId) { this.deptId = deptId; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public LoginUser() { } public LoginUser(SysUser user, Set&amp;lt;String&amp;gt; permissions) { this.user = user; this.permissions = permissions; } public LoginUser(Long userId, Long deptId, SysUser user, Set&amp;lt;String&amp;gt; permissions) { this.userId = userId; this.deptId = deptId; this.user = user; this.permissions = permissions; } @JSONField(serialize = false) @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } /** * 账户是否未过期,过期无法验证 */ @JSONField(serialize = false) @Override public boolean isAccountNonExpired() { return true; } /** * 指定用户是否解锁,锁定的用户无法进行身份验证 * * @return */ @JSONField(serialize = false) @Override public boolean isAccountNonLocked() { return true; } /** * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 * * @return */ @JSONField(serialize = false) @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 ,禁用的用户不能身份验证 * * @return */ @JSONField(serialize = false) @Override public boolean isEnabled() { return true; } public Long getLoginTime() { return loginTime; } public void setLoginTime(Long loginTime) { this.loginTime = loginTime; } public String getIpaddr() { return ipaddr; } public void setIpaddr(String ipaddr) { this.ipaddr = ipaddr; } public String getLoginLocation() { return loginLocation; } public void setLoginLocation(String loginLocation) { this.loginLocation = loginLocation; } public String getBrowser() { return browser; } public void setBrowser(String browser) { this.browser = browser; } public String getOs() { return os; } public void setOs(String os) { this.os = os; } public Long getExpireTime() { return expireTime; } public void setExpireTime(Long expireTime) { this.expireTime = expireTime; } public Set&amp;lt;String&amp;gt; getPermissions() { return permissions; } public void setPermissions(Set&amp;lt;String&amp;gt; permissions) { this.permissions = permissions; } public SysUser getUser() { return user; } public void setUser(SysUser user) { this.user = user; } @Override public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() { return null; } }</code></pre> <h2>5.2 UserDetailsService和其实现UserDetailsServiceImpl</h2> <p>UserDetailService只负责从特定的地方(通常是数据库)加载用户信息,自定义UserDetailsService如下:</p> <pre><code class="language-java">/** * 用户验证处理 * * @author ruoyi */ @Service public class UserDetailsServiceImpl implements UserDetailsService { private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); @Autowired private ISysUserService userService; @Autowired private SysPermissionService permissionService; /** * 获取数据库中username对应的用户信息,并封装成UserDetails类 * @param username 1 * @return: org.springframework.security.core.userdetails.UserDetails * @author: gefeng * @date: 2022/6/13 19:14 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = userService.selectUserByUserName(username); if (StringUtils.isNull(user)) { log.info(&amp;quot;登录用户:{} 不存在.&amp;quot;, username); throw new ServiceException(&amp;quot;登录用户:&amp;quot; + username + &amp;quot; 不存在&amp;quot;); } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { log.info(&amp;quot;登录用户:{} 已被删除.&amp;quot;, username); throw new ServiceException(&amp;quot;对不起,您的账号:&amp;quot; + username + &amp;quot; 已被删除&amp;quot;); } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { log.info(&amp;quot;登录用户:{} 已被停用.&amp;quot;, username); throw new ServiceException(&amp;quot;对不起,您的账号:&amp;quot; + username + &amp;quot; 已停用&amp;quot;); } return createLoginUser(user); } public UserDetails createLoginUser(SysUser user) { //根据userId、部门id、系统用户和用户权限集合(根据用户ID查询)创建LoginUser对象 return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); } }</code></pre> <h1>6. 授权流程</h1> <h2>6.1 授权流程</h2> <p>Spring Security可以通过http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。 Spring Security的授权流程如下: <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=0f8630b4662a7a516f349eafc18cffc3&amp;amp;file=file.png" alt="" /></p> <p>分析授权流程:</p> <ol> <li>拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor 的子 类拦截。</li> <li>获取资源访问策略,FilterSecurityInterceptor会从SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection&lt;ConfigAttribute&gt; 。 SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读 取访问策略如: <pre><code class="language-java">http .authorizeRequests(    ) .antMatchers(&amp;quot;/r/r1&amp;quot;).hasAuthority(&amp;quot;p1&amp;quot;) .antMatchers(&amp;quot;/r/r2&amp;quot;).hasAuthority(&amp;quot;p2&amp;quot;)</code></pre></li> <li>最后,FilterSecurityInterceptor会调用AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。</li> </ol> <h2>6.2 授权决策</h2> <p>AccessDecisionManager(访问决策管理器)的核心接口如下:</p> <pre><code class="language-java">public interface AccessDecisionManager { /** * 通过传递的参数来决定用户是否有访问对应受保护资源的权限 */ void decide(Authentication authentication , Object object, Collection&amp;lt;ConfigAttribute&amp;gt; configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException; //略.. }</code></pre> <p>这里着重说明一下decide的参数:</p> <ul> <li>authentication:要访问资源的访问者的身份</li> <li>object:要访问的受保护资源,web请求对应FilterInvocation</li> <li>configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。</li> <li>decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。</li> </ul> <p>AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。 <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=1b43706e3794dcff397c0c38d84e3150&amp;amp;file=file.png" alt="" /> 通过上图可以看出,AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。</p> <pre><code class="language-java">public interface AccessDecisionVoter&amp;lt;S&amp;gt; { int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = ‐1; boolean supports(ConfigAttribute var1); boolean supports(Class&amp;lt;?&amp;gt; var1); int vote(Authentication var1, S var2, Collection&amp;lt;ConfigAttribute&amp;gt; var3); }</code></pre> <p>vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意, ACCESS_DENIED表示拒绝,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前 Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。</p> <h2>6.4 Web授权</h2> <p>通过给http.authorizeRequests() 添加多个子节点来定制需求到我们的URL,如下代码:</p> <pre><code class="language-java">@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() //(1) .antMatchers(&amp;quot;/r/r1&amp;quot;).hasAuthority(&amp;quot;p1&amp;quot;) //(2) .antMatchers(&amp;quot;/r/r2&amp;quot;).hasAuthority(&amp;quot;p2&amp;quot;) //(3) .antMatchers(&amp;quot;/r/r3&amp;quot;).access(&amp;quot;hasAuthority('p1') and hasAuthority('p2')&amp;quot;) //(4) .antMatchers(&amp;quot;/r/**&amp;quot;).authenticated() //(5) .anyRequest().permitAll() //(6) .and() .formLogin() // ... }</code></pre> <p>(1)http.authorizeRequests() 方法有多个子节点,每个macher按照他们的声明顺序执行。 (2)指定&quot;/r/r1&quot;URL,拥有p1权限能够访问 (3)指定&quot;/r/r2&quot;URL,拥有p2权限能够访问 (4)指定了&quot;/r/r3&quot;URL,同时拥有p1和p2权限才能够访问 (5)指定了除了r1、r2、r3之外&quot;/r/**&quot;资源,同时通过身份认证就能够访问,这里使用SpEL(Spring Expression Language)表达式。 (6)剩余的尚未匹配的资源,不做保护。</p> <p>注意: 规则的顺序是重要的,更具体的规则应该先写.现在以/admin开始的所有内容都需要具有ADMIN角色的身份验证用户,即使是/admin/login路径(因为/admin/login已经被/admin/**规则匹配,因此第二个规则被忽略).</p> <pre><code class="language-java">.antMatchers(&amp;quot;/admin/**&amp;quot;).hasRole(&amp;quot;ADMIN&amp;quot;) .antMatchers(&amp;quot;/admin/login&amp;quot;).permitAll()</code></pre> <p>因此,登录页面的规则应该在/ admin / **规则之前.例如.</p> <pre><code class="language-java">.antMatchers(&amp;quot;/admin/login&amp;quot;).permitAll() .antMatchers(&amp;quot;/admin/**&amp;quot;).hasRole(&amp;quot;ADMIN&amp;quot;)</code></pre> <p>保护URL常用的方法有:</p> <ul> <li>authenticated() 保护URL,需要用户登录</li> <li>permitAll() 指定URL无需保护,一般应用与静态资源文件</li> <li>hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较.</li> <li>hasAuthority(String authority) 限制单个权限访问</li> <li>hasAnyRole(String… roles)允许多个角色访问.</li> <li>hasAnyAuthority(String… authorities) 允许多个权限访问.</li> <li>access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制.</li> <li>hasIpAddress(String ipaddressExpression) 限制IP地址或子网</li> </ul> <h2>6.5 方法授权</h2> <p>从Spring Security2.0版 本开始,它支持服务层方法的安全性的支持。本节学习@PreAuthorize,@PostAuthorize, @Secured三类注解。我们可以在任何@Configuration 实例上使用@EnableGlobalMethodSecurity 注释来启用基于注解的安全性。以下内容将启用Spring Security的@Secured 注释。</p> <pre><code class="language-java">/** * spring security配置 * prePostEnabled = true, securedEnabled = true:开启了三个注解 * @PreAuthorize:方法执行前进行权限检查 * @PostAuthorize:方法执行后进行权限检查 * @Secured:类似于 @PreAuthorize * @author ruoyi */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {//...}</code></pre> <p>然后向方法(在类或接口上)添加注解就会限制对该方法的访问。 Spring Security的原生注释支持为该方法定义了一组属性。 这些将被传递给AccessDecisionManager以供它作出实际的决定:</p> <pre><code class="language-java">public interface BankService { @Secured(&amp;quot;IS_AUTHENTICATED_ANONYMOUSLY&amp;quot;) public Account readAccount(Long id); @Secured(&amp;quot;IS_AUTHENTICATED_ANONYMOUSLY&amp;quot;) public Account[] findAccounts(); @Secured(&amp;quot;ROLE_TELLER&amp;quot;) public Account post(Account account, double amount); }</code></pre> <p>以上配置标明readAccount、findAccounts方法可匿名访问,底层使用WebExpressionVoter投票器,可从 AffirmativeBased第23行代码跟踪。post方法需要有TELLER角色才能访问,底层使用RoleVoter投票器。</p> <pre><code class="language-java">public interface BankService { @PreAuthorize(&amp;quot;isAnonymous()&amp;quot;) public Account readAccount(Long id); @PreAuthorize(&amp;quot;isAnonymous()&amp;quot;) public Account[] findAccounts(); @PreAuthorize(&amp;quot;hasAuthority('p_transfer') and hasAuthority('p_read_account')&amp;quot;) public Account post(Account account, double amount); }</code></pre> <p>以上配置标明readAccount、findAccounts方法可匿名访问,post方法需要同时拥有p_transfer和p_read_account权限才能访问,底层使用WebExpressionVoter投票器,可从AffirmativeBased第23行代码跟踪。</p> <h1>7. Spring Security过滤器链</h1> <p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=04b18aee7adf79927e5e3849cecc3acc&amp;amp;file=file.png" alt="" /></p> <ol> <li> <p>SecurityContextPersistenceFilter:这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;</p> </li> <li> <p>UsernamePasswordAuthenticationFilter: 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,这些都可以根据需求做相关改变;</p> </li> <li> <p>ExceptionTranslationFilter: 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。</p> </li> <li>FilterSecurityInterceptor: 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;</li> </ol> <h1>8. RuoYi-Vue自定义Handler、Filter和Interceptor</h1> <h2>8.1 认证失败处理类-AuthenticationEntryPointImpl</h2> <p>自定义AuthenticationEntryPointImpl实现AuthenticationEntryPoint</p> <pre><code class="language-java">/** * 认证失败处理类 返回未授权响应(HttpStatus.UNAUTHORIZED-401) * * @author ruoyi */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { int code = HttpStatus.UNAUTHORIZED; String msg = StringUtils.format(&amp;quot;请求访问:{},认证失败,无法访问系统资源&amp;quot;, request.getRequestURI()); ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); } }</code></pre> <p>在SecurityConfig中添加配置</p> <pre><code class="language-java">@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // 防止CSRF功能禁用,因为不使用session .csrf().disable() // 认证失败处理类(implements AuthenticationEntryPoint) .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) ... ... }</code></pre> <h2>8.2 退出登录处理类-LogoutSuccessHandlerImpl</h2> <p>自定义LogoutSuccessHandlerImpl实现LogoutSuccessHandler</p> <pre><code class="language-java">/** * 自定义退出处理类 返回操作成功(HttpStatus.SUCCESS-200) * * @author ruoyi */ @Configuration public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { @Autowired private TokenService tokenService; /** * 退出处理 * 0. 从request中获取登录用户信息 * 1. 删除用户缓存记录 * 2. 记录用户退出日志 * * @return */ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { LoginUser loginUser = tokenService.getLoginUser(request); if (StringUtils.isNotNull(loginUser)) { String userName = loginUser.getUsername(); // 删除用户缓存记录 tokenService.delLoginUser(loginUser.getToken()); // 记录用户退出日志 AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, &amp;quot;退出成功&amp;quot;)); } ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, &amp;quot;退出成功&amp;quot;))); } }</code></pre> <p>在SecurityConfig中添加配置</p> <pre><code class="language-java">@Override protected void configure(HttpSecurity httpSecurity) throws Exception { ... ... //退出登录处理:前端请求/logout就会进入处理类 httpSecurity.logout().logoutUrl(&amp;quot;/logout&amp;quot;).logoutSuccessHandler(logoutSuccessHandler); ... ... }</code></pre> <h2>8.3 token认证过滤器-JwtAuthenticationTokenFilter</h2> <p>自定义JwtAuthenticationTokenFilter继承OncePerRequestFilter</p> <pre><code class="language-java">/** * token过滤器 验证token有效性 * * @author ruoyi */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private TokenService tokenService; /** * 1.获取登录用户的身份信息(1.1从请求头中获取token,1.2从token中解析出uuid,1.3拼接出userKey去redis中获取value即为用户信息) * 2.刷新token缓存有效期 * 3.将用户信息设置到SpringSecurity上下文 * 4.chain.doFilter(request, response)进入后续的过滤器(UsernamePasswordAuthenticationFilter)处理 * @param request 1 * @param response 2 * @param chain 3 * @return: void * @author: gefeng * @date: 2022/6/14 11:02 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { //1.获取登录用户的身份信息(1.1从请求头中获取token,1.2从token中解析出uuid,1.3拼接出userKey去redis中获取value即为用户信息) LoginUser loginUser = tokenService.getLoginUser(request); if (StringUtils.isNotNull(loginUser) &amp;amp;&amp;amp; StringUtils.isNull(SecurityUtils.getAuthentication())) { //2.刷新token缓存有效期 tokenService.verifyToken(loginUser); //3.将用户信息设置到上下文 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } //用户信息为空,则未登录过滤掉 //进入后续的过滤器处理 chain.doFilter(request, response); } }</code></pre> <p>在SecurityConfig中添加配置,在UsernamePasswordAuthenticationFilter之前</p> <pre><code class="language-java">@Override protected void configure(HttpSecurity httpSecurity) throws Exception { ... ... // 添加JWT filter(在UsernamePasswordAuthenticationFilter之前) httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); ... ... }</code></pre> <h2>8.4 跨域过滤器-CorsFilter</h2> <pre><code class="language-java">/** * 跨域配置 */ @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置访问源地址 config.addAllowedOriginPattern(&amp;quot;*&amp;quot;); // 设置访问源请求头 config.addAllowedHeader(&amp;quot;*&amp;quot;); // 设置访问源请求方法 config.addAllowedMethod(&amp;quot;*&amp;quot;); // 有效期 1800秒 config.setMaxAge(1800L); // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(&amp;quot;/**&amp;quot;, config); // 返回新的CorsFilter return new CorsFilter(source); }</code></pre> <p>在SecurityConfig中添加配置,在JwtAuthenticationTokenFilter和LogoutFilter之前</p> <pre><code class="language-java">@Override protected void configure(HttpSecurity httpSecurity) throws Exception { ... ... // 添加CORS filter(在JwtAuthenticationTokenFilter和LogoutFilter之前) httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); ... ... }</code></pre> <h2>8.5 防止重复提交拦截器-RepeatSubmitInterceptor</h2> <p>先自定义一个抽象类RepeatSubmitInterceptor实现HandlerInterceptor</p> <pre><code class="language-java">/** * 防止重复提交拦截器抽象类 * * @author ruoyi */ @Component public abstract class RepeatSubmitInterceptor implements HandlerInterceptor { /** * 对加了@RepeatSubmit注解的方法进行重复提交判断(判断规则由子类实现),若重复提交,进行拦截,响应报文返回码500和提示 * @param request 1 * @param response 2 * @param handler 3 * @return: boolean * @author: gefeng * @date: 2023/4/6 14:39 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request, annotation)) { AjaxResult ajaxResult = AjaxResult.error(annotation.message()); ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult)); return false; } } return true; } else { return true; } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 * * @param request * @return * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); }</code></pre> <p>再自定义一个子类SameUrlDataInterceptor继承RepeatSubmitInterceptor实现重复提交的判断规则</p> <pre><code class="language-java">/** * 判断请求url和数据是否和上一次相同, * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 * * @author ruoyi */ @Component public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public final String REPEAT_PARAMS = &amp;quot;repeatParams&amp;quot;; public final String REPEAT_TIME = &amp;quot;repeatTime&amp;quot;; // 令牌自定义标识 @Value(&amp;quot;${token.header}&amp;quot;) private String header; @Autowired private RedisCache redisCache; /** * 1. nowDataMap记录请求参数和请求时间 * 2. Redis中保存信息的key=指定key + url + 消息头,value=cacheMap&amp;lt;url,nowDataMap&amp;gt; * 3. 校验本次请求url的参数和之前相同url请求的参数是否相同,若相同再比较相隔时间若小于@RepeatSubmit设置的间隔时间,则表示为重复请求 * * @param request 1 * @param annotation 2 * @return: boolean * @author: gefeng * @date: 2023/4/6 14:45 */ @SuppressWarnings(&amp;quot;unchecked&amp;quot;) @Override public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) { String nowParams = &amp;quot;&amp;quot;; if (request instanceof RepeatedlyRequestWrapper) { RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; nowParams = HttpHelper.getBodyString(repeatedlyRequest); } // body参数为空,获取Parameter的数据 if (StringUtils.isEmpty(nowParams)) { nowParams = JSONObject.toJSONString(request.getParameterMap()); } Map&amp;lt;String, Object&amp;gt; nowDataMap = new HashMap&amp;lt;String, Object&amp;gt;(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放cache的key值) String url = request.getRequestURI(); // 取消息头做唯一值(没有消息头则使用请求地址) String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); // 唯一标识(指定key + url + 消息头) String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + url + submitKey; Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); if (sessionObj != null) { Map&amp;lt;String, Object&amp;gt; sessionMap = (Map&amp;lt;String, Object&amp;gt;) sessionObj; if (sessionMap.containsKey(url)) { Map&amp;lt;String, Object&amp;gt; preDataMap = (Map&amp;lt;String, Object&amp;gt;) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) &amp;amp;&amp;amp; compareTime(nowDataMap, preDataMap, annotation.interval())) { return true; } } } Map&amp;lt;String, Object&amp;gt; cacheMap = new HashMap&amp;lt;String, Object&amp;gt;(); cacheMap.put(url, nowDataMap); redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map&amp;lt;String, Object&amp;gt; nowMap, Map&amp;lt;String, Object&amp;gt; preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map&amp;lt;String, Object&amp;gt; nowMap, Map&amp;lt;String, Object&amp;gt; preMap, int interval) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) &amp;lt; interval) { return true; } return false; } }</code></pre>

页面列表

ITEM_HTML