用户授权

基于角色或权限进行访问

1、配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //配置没有权限访问所跳转的自定义403页面
        http.exceptionHandling().accessDeniedPage("/403.html");

        //开启自定义登陆页面
        http.formLogin()    
            //设置登陆页面
            .loginPage("/index.html")
            //登录访问路径
            .loginProcessingUrl("/user/login")
            //登录成功后,所跳转路径
            .defaultSuccessUrl("/test/success").permitAll()
            .and()
            //设置不需要认证,可以直接访问的页面
            .authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll()
            //所有用户都可以访问
            .anyRequest().authenticated();
    }
}

2、hasAuthority

/*
指定URL需要一个特定的授权
形参:
authority - 需要的权限(即 ROLE_USER, ROLE_ADMIN, 等等)
返回值:
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
    return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}

private static String hasAuthority(String authority) {
    return "hasAuthority('" + authority + "')";
}

(1)针对允许某一权限的操作

(2)判断当前主体是否具有指定权限,有返回 true,没有返回 false

(3)在配置类设置,当前访问地址所需权限

//当前登录用户,只有具有admin权限才可以访问指定路径
http.antMatchers("/test/index").hasAuthority("admin")

(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面

3、hasAnyAuthority

/*
指定URL需要输入权限中的任何一个
形参:
authority - 请求至少需要一个授权(例如,"ROLE_USER", "ROLE_ADMIN "意味着需要 "ROLE_USER "或 "ROLE_ADMIN")
返回值:
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) {
    return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities));
}

private static String hasAnyAuthority(String... authorities) {
    String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
    return "hasAnyAuthority('" + anyAuthorities + "')";
}

(1)针对允许多个权限的操作

(2)如果当前主体拥有任一权限,返回 true,否则返回 false

(3)在配置类设置,当前访问地址所需权限

//当前登录用户,只有具有admin权限才可以访问指定路径,参数为多个权限字符串,或使用,分隔包含多个权限的一个字符串
http.antMatchers("/test/index").hasAnyAuthority("admin","manager")

(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面

4、hasRole

/*
用于指定需要特定角色的URL的快捷方式。如果你不想让角色前缀(默认为 "ROLLE_")自动插入,请参阅hasAuthority(String)
形参:
role - 需要的角色(例如:USER、ADMIN等)。注意,不应该以ROLE_开始,因为这将被自动插入
返回值:
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasRole(String role) {
    return access(ExpressionUrlAuthorizationConfigurer
                  .hasRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, role));
}

private static String hasRole(String rolePrefix, String role) {
    Assert.notNull(role, "role cannot be null");
    Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> "role should not start with '"
                  + rolePrefix + "' since it is automatically inserted. Got '" + role + "'");
    return "hasRole('" + rolePrefix + role + "')";
}

(1)针对允许某一权限的操作

(2)如果当前主体具有指定的角色,则返回 true,否则返回 false

(3)在配置类设置,当前访问地址所需权限

//添加默认前缀ROLE_,拼接为ROLE_admin
http.antMatchers("/test/index").hasAuthority("admin")

(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");

(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面

5、hasAnyRole

/*
指定URL需要许多角色中的任何一个的快捷方式。如果你不想让角色前缀(默认为 "ROLLE_")自动插入,请参阅hasAnyAuthority(String...)
形参:
roles - 需要的角色(例如:USER、ADMIN等)。注意,它不应该以ROLE_开始,因为它会被自动插入
返回值;
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasAnyRole(String... roles) {
    return access(ExpressionUrlAuthorizationConfigurer
                  .hasAnyRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, roles));
}

private static String hasAnyRole(String rolePrefix, String... authorities) {
    String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','" + rolePrefix);
    return "hasAnyRole('" + rolePrefix + anyAuthorities + "')";
}

(1)针对允许多个权限的操作

(2)如果当前主体拥有任一权限,返回 true,否则返回 false

(3)在配置类设置,当前访问地址所需权限

//添加默认前缀ROLE_,拼接所有角色,参数为多个权限字符串,或使用,分隔包含多个权限的一个字符串
http.antMatchers("/test/index").hasAnyAuthority("admin","manager")

(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");

(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面

 

使用注解认证授权

1、需要开启注解支持,注解在启动类上

@EnableGlobalMethodSecurity(securedEnabled=true)

2、@Secured

(1)设置哪些角色可以访问方法,需要添加前缀,默认为 ROLE_

(2)示例

@RequestMapping("/testSecured")
@ResponseBody
@Secured({"ROLE_admin","ROLE_user"})
public String testSecured(){
    return "testSecured";
}

3、@PreAuthorize

(1)进入方法前的权限验证

(2)示例

@RequestMapping("/testPreAuthorize")
@ResponseBody
//可使用hasRole、hasAnyRole、hasAuthority、hasAnyAuthority
@PreAuthorize("hasAuthority('admin')")
public String testPreAuthorize(){
    return "testPreAuthorize";
}

4、@PostAuthorize

(1)在方法执行后,再进行权限验证,适合验证带有返回值的权限,较少使用

(2)示例

@RequestMapping("testPostAuthorize")
@ResponseBody
//可使用hasRole、hasAnyRole、hasAuthority、hasAnyAuthority
@PostAuthorize("hasAuthority('admin')")
public String testPostAuthorize(){
    return "testPostAuthorize";
}

5、@PostFilter

(1)权限验证之后,对返回数据进行过滤,留下符合条件的数据,较少使用

(2)示例

@RequestMapping("testPostFilter")
@ResponseBody
//表示只保留username == 'admin'的数据
@PostFilter("filterObject.username == 'admin'")
public List<UserInfo> testPostFilter(){
    List<UserInfo> list = new ArrayList<>();
    list.add(new UserInfo(1,"admin","1234"));
    list.add(new UserInfo(2,"user","1234"));
    return list;
}

6、@PreFilter

(1)进入控制器之前,对传入数据进行过滤,较少使用

(2)示例

@RequestMapping("testPreFilter")
@ResponseBody
//表示只接收username == 'user'的数据
@PostFilter("filterObject.username == 'user'")
public List<UserInfo> testPreFilter(){
    List<UserInfo> list = new ArrayList<>();
    list.add(new UserInfo(1,"admin","1234"));
    list.add(new UserInfo(2,"user","1234"));
    return list;
}

 

用户注销

1、登录页面添加退出链接

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        登录成功
        <br>
        <a href="/logout">退出</a>
    </body>
</html>

2、在配置类中添加退出映射地址

http.logout()
    //设置退出路径
    .logoutUrl("/logout")
    //退出成功后,所跳转的路径
    .logoutSuccessUrl("/index")
    //允许所有用户
    .permitAll();

 

自动登录

1、原理

(1)第一次登陆:客户端存放 Cookie 加密串,数据库存放加密串、用户信息字符串

(2)再次登录:获取 Cookie 信息,与数据库的加密串比对,认证成功,自动登录

2、Spring Security 底层

(1)第一次认证请求:UsernamePasswordAuthenticationFilter

(2)认证成功:RememberMeServices 使用 TokenReposity 生成 Token,将 Token 写入浏览器 Cookie,将 Token 写入数据库

(3)再次请求:RememberMeAuthenticationFilter

(4)RememberMeServices 读取 Cookie 中的 Token,UserDetailsService 查找数据库中的 Token,比对两者

3、实现示例

(1)表结构

CREATE TABLE `persistent logins`
(
    `username`  VARCHAR(64NOT NULL,
                        `series`  VARCHAR64NOT NULL,
                        `token` VARCHAR64NOT NULL,
                        `last_used`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                        PRIMARY KEY (`series`)
                       ) 
    ENGINE=INNODB DEFAULT CHARSET=utf8;

(2)配置类:注入数据源;配置操作数据库对象 JdbcTokenRepositoryImpl;配置自动登录

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //
    @Autowired
    private UserDetailsService userDetailsService;
    
    //注入数据源
    @Autowired
    private DataSource dataSource;

    //配置操作数据库对象:JdbcTokenRepositoryImpl,为PersistentTokenRepository的实现类
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        //设置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        //自动创建表
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //配置没有权限访问所跳转的自定义403页面
        http.exceptionHandling().accessDeniedPage("/403.html");

        //开启自定义登陆页面
        http.formLogin()    
            //设置登陆页面
            .loginPage("/index.html")
            //登录访问路径
            .loginProcessingUrl("/user/login")
            //登录成功后,所跳转路径
            .defaultSuccessUrl("/test/success").permitAll()
            .and()
            //设置不需要认证,可以直接访问的页面
            .authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll()
            //所有用户都可以访问
            .anyRequest().authenticated()
            //指定已经记住的用户允许使用的URL
            .and().rememberMe()
            //指定要使用的PersistentTokenRepository,默认使用TokenBasedRememberMeServices
            .tokenRepository(persistentTokenRepository())
            //设置自动登录的有效时长,单位为秒
            .tokenValiditySeconds(60)
            //设置userDetailsService操作数据库
            .userDetailsService(userDetailsService);
    }
}

(3) 登录页面添加复选框 remember-me,该复选框的 name 属性固定名称

记住密码<input type="checkbox" name="remeber-me" title="下次自动登录"/><br/>

 

CSRF

1、Cross-site request forgery:跨站请求伪造

2、一种挟制用户在当前已登录的 Web 应用程序上,执行非本意的操作的攻击方法:攻击者通过一些技术手段欺骗用户的浏览器,去访问一个自己曾经认证过的网站并运行一些操作,由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行

3、与跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任

4、利用 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

5、从 Spring Security 4.0 开始,默认启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 针对 PATCH、POST、PUT、DELETE 方法进行防护

6、页面表单添加隐藏域

<!--
    使用Thymeleaf语法
    传入参数名、Token值
-->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

7、Spring Security 实现 CSRF 原理

(1)第一次请求:CsrfTokenRepository 生成 CsrfToken,保存到 HttpSession、Cookie 中

(2)再次请求:Cookie 携带 CsefToken 字符串,与 Session 储存的 Token 比对,CsrfFilter 判断是否合法

8、源码

(1)CsrfToken 接口

public interface CsrfToken extends Serializable {

    //获取CSRF在响应中填入的HTTP头名称,并可以放在请求中,而不是参数中,不能为空
    String getHeaderName();

    //获取应包含Token的HTTP参数名称,不能为空
    String getParameterName();

    //获取令牌值,不能为空
    String getToken();

}

(2)CsrfToken 实现类:SaveOnAccessCsrfToken(LazyCsrfTokenRepository 的静态内部类)、DefaultCsrfToken(默认)

(3)SaveOnAccessCsrfToken 中,当开发者调用 getToken 想要去获取 CsrfToken 时,才会去对 CsrfToken 做保存操作,调用 HttpSessionCsrfTokenRepository 或 CookieCsrfTokenRepository 的 saveToken 方法

public String getToken() {
    saveTokenIfNecessary();
    return this.delegate.getToken();
}

private void saveTokenIfNecessary() {
    if (this.tokenRepository == null) {
        return;
    }
    synchronized (this) {
        if (this.tokenRepository != null) {
            this.tokenRepository.saveToken(this.delegate, this.request, this.response);
            this.tokenRepository = null;
            this.request = null;
            this.response = null;
        }
    }
}

(4)CsrfTokenRepository 接口

public interface CsrfTokenRepository {

    /*
    生成一个CsrfToken
    形参:
    request - 要使用的HttpServletRequest
    返回值:
    被生成的CsrfToken,不能为空
    */
    CsrfToken generateToken(HttpServletRequest request);

    /*
    使用HttpServletRequest和HttpServletResponse保存CsrfToken,如果CsrfToken为空,则表示删除它
    形参:
    token - 要保存的CsrfToken或删除的空值
    request - 使用的HttpServletRequest
    response - 要使用的HttpServletResponse
    */
    void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);

    /*
    从HttpServletRequest中加载预期的CsrfToken
    形参:
    request - 要使用的HttpServletRequest
    返回值:
    CsrfToken,如果不存在则为空
    */
    CsrfToken loadToken(HttpServletRequest request);

}

(5)CsrfTokenRepository 实现类:CookieCsrfTokenRepository、HttpSessionCsrfTokenRepository + LazyCsrfTokenRepository(默认)

(6)LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,而是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或 CookieCsrfTokenRepository 的功能

/*
生成一个新的令牌
形参:
request - 要使用的HttpServletRequest,HttpServletRequest必须有HttpServletResponse作为属性,名称为HttpServletResponse.class.getName()
*/
public CsrfToken generateToken(HttpServletRequest request) {
    return wrap(request, this.delegate.generateToken(request));
}

/*
如果CsrfToken不是空的,什么都不做;如果它是空的,那么就立即执行保存
只有当CsrfToken.getToken()从generateToken(HttpServletRequest)中被访问时才会进行保存
*/
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
    if (token == null) {
        this.delegate.saveToken(token, request, response);
    }
}

(7)CsrfFilter 类

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
    request.setAttribute(HttpServletResponse.class.getName(), response);
    //从CsrfTokenRepository中获取CsrfToken
    CsrfToken csrfToken = this.tokenRepository.loadToken(request);
    boolean missingToken = (csrfToken == null);
    //如果找不到CsrfToken,说明该请求是第一次发起
    if (missingToken) {
        //生成一个CsrfToken
        csrfToken = this.tokenRepository.generateToken(request);
        //保存到CsrfTokenRepository
        this.tokenRepository.saveToken(csrfToken, request, response);
    }
    //在请求中添加CsrfToken,默认情况下,通过JSP或Thymeleaf标签渲染_csrf的数据来源
    request.setAttribute(CsrfToken.class.getName(), csrfToken);
    request.setAttribute(csrfToken.getParameterName(), csrfToken);
    //判断哪些请求方法需要做校验
    if (!this.requireCsrfProtectionMatcher.matches(request)) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Did not protect against CSRF since request did not match "
                              + this.requireCsrfProtectionMatcher);
        }
        filterChain.doFilter(request, response);
        return;
    }
    //先从请求头中获取CsrfToken
    String actualToken = request.getHeader(csrfToken.getHeaderName());
    //请求头无法获取CsrfToken,再从请求参数中获取
    if (actualToken == null) {
        actualToken = request.getParameter(csrfToken.getParameterName());
    }
    //如果请求所携带的CsrfToken与从Repository中获取的不同,则抛出异常
    if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
        this.logger.debug(
            LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
        AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
            : new MissingCsrfTokenException(actualToken);
        this.accessDeniedHandler.handle(request, response, exception);
        return;
    }
    //正常情况下继续执行过滤器链的后续流程
    filterChain.doFilter(request, response);
}
private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;

public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();

private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {

    //表示GET、HEAD、TRACE、OPTIONS请求不受保护
    private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

    @Override
    public boolean matches(HttpServletRequest request) {
        return !this.allowedMethods.contains(request.getMethod());
    }

    @Override
    public String toString() {
        return "CsrfNotRequired " + this.allowedMethods;
    }

}
posted @   半条咸鱼  阅读(225)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示