SpringSecurity(二)用户认证和用户授权

上一篇通过一个入门案例引入Spring Security,然后居于内存进行登录用户定制。本文来学习用户认证定制和用户授权相关知识。自带的登录认证与登录拦截存在很多不方便,开发中一般不使用默认认证,往往需要根据项目定制。

一、用户认证定制

开发中,项目都有自己的登录页面,用不上默认的登录页面,而且默认的登录页面样式也不好看,这时就需要自己定制。一但定制后,原先的页面默认自动认证都会失效,都需要重新定制。

1.1、定制登录页面

步骤1: 定制登陆页面/resources/static/login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org/" lang="en">
  <head>
    <meta charset="UTF-8">
    <title>用户登录</title>
  </head>
  <body>
    <h1>用户登录</h1>
    <form action="/login" method="post">
      用户名:<input type="text" name="username"> <br>
      密码:<input type="text" name="password"><br>
      <input type="submit" value="登录">
    </form>
  </body>
</html>

步骤2: 修改 SecurityConfig 继承 WebSecurityConfigurerAdapter,重写 configure(HttpSecurity http)

package com.duan.config;

import com.duan.handler.MyAuthenticationFailureHandler;
import com.duan.handler.MyAuthenticationSuccessHandler;
import com.duan.handler.MyLogoutSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author db
 * @version 1.0
 * @description SecurityConfig
 * @since 2024/7/15
 */
@Configuration
public class SecurityConfig  extends WebSecurityConfigurerAdapter {


    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).authorities("p1").build());
        manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).authorities("p2").build());
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用csrf保护
        http.csrf().disable();

        // 请求url权限控制
        http.authorizeRequests()
         //匹配 /login.html 路径,permitAll() 不限制(登录/不登录都可以访问该路径,不进行登录检查)
        .antMatchers("/login.html").permitAll()
        .antMatchers("/login").permitAll()
        //除去上面匹配路径外,剩下的路径都需要进行登录检查
        .anyRequest().authenticated();

        //用户登录控制
        http.formLogin()
            //指定登录页面
           .loginPage("/login.html")
             //指定登录路径
           .loginProcessingUrl("/login");
    }

}

API解释

  • http.csrf().disable(): 禁用 csrf 保护;Spring security为防止CSRF(Cross-site request forgery跨站请求伪造) 的发生,限制了除了GET以外的大多数方法。
  • http.authorizeRequests():请求url权限控制,可以进行路径,权限配置。
  • http.formLogin(): 用户登录控制,控制登录相关操作。

启动项目,浏览器访问http://localhost:8183/testMethod,界面跳到定制的登陆页面。

1.2、定制登录成功跳转路径

Spring Security 登录成功之后,默认跳转到上一个路径,如果需要定制跳转路径,可以使用下面配置。

//用户登录控制
http.formLogin()
    .successForwardUrl("/success")

controller中新建 success方法。

@RequestMapping("/success")
public String success(){
    return "success";
}

测试:登录成功观察跳转。

1.3、定制登录失败跳转路径

Spring Security 登录失败之后,默认跳转到登录页面,如果需要定制跳转路径,可以使用下面配置。

//用户登录控制
http.formLogin()
    .failureForwardUrl("/fail")

登录失败,跳转/fail路径,因为没有登录,需要放行/fail 登录检查。

http.authorizeRequests()
    .antMatchers("/fail").permitAll()

controller中新建 fail方法。

@RequestMapping("/fail")
public String fail(){
    return "fail";
}

测试:登录失败观察跳转。

1.4、定制登录成功逻辑

前面定制登录成功后使用successForwardUrl 跳转某个接口路径,如果是前后端分离项目,要求返回是Json格式,那怎么办?此时可以使用登录成功处理器。

新建 MyAuthenticationSuccessHandler类实现AuthenticationSuccessHandler

package com.duan.handler;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author db
 * @version 1.0
 * @description MyAuthenticationSuccessHandler
 * @since 2024/7/20
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //返回json
        response.setContentType("application/json;charset=utf-8");
        String data = "{\"code\":200, \"msg\":\"登录成功\", \"data\":{}}";
        response.getWriter().write(data);
    }
}

securityConfig 中配置登录成功处理器。

//用户登录控制
http.formLogin()
    .successHandler(new MyAuthenticationSuccessHandler());

测试:登录成功,观察效果。

1.5、定制登陆失败逻辑

与登录成功处理器对比,也有登录失败处理器。

新建MyAuthenticationFailureHandler类实现AuthenticationFailureHandler

package com.duan.handler;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author db
 * @version 1.0
 * @description MyAuthenticationFailureHandler
 * @since 2024/7/20
 */
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //返回json
        response.setContentType("application/json;charset=utf-8");
        String data = "{\"code\":500, \"msg\":\"登录失败\", \"data\":{\"error\":"+exception.getMessage()+"}}";
        response.getWriter().write(data);
    }

}

securityConfig中配置登录成功处理器。

//用户登录控制
http.formLogin()
    .failureHandler(new MyAuthenticationFailureHandler())

测试:登录失败,观察效果。

1.6、定制登出成功跳转路径

Spring Security 登录成功之后,默认跳转登录页面,如果有需要可以定制跳转路径。

//用户登出控制
http.logout()
    .logoutSuccessUrl("/logoutsuccess")

登出之后,logoutsuccess 要放行。

//请求url权限控制
http.authorizeRequests()
    .antMatchers("/logoutsuccess").permitAll()
    .anyRequest().authenticated();

controller中新建logoutsuccess方法。

    @RequestMapping("/logoutSuccess")
    public String logoutsuccess(){
        return "logoutSuccess";
    }

测试:登出成功,观察效果。

1.7、定制登出成功逻辑

登录成功有逻辑定制,登出成功也可以定制。

新建MyLogoutSuccessHandler实现LogoutSuccessHandler

package com.duan.handler;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author db
 * @version 1.0
 * @description MyLogoutSuccessHandler
 * @since 2024/7/20
 */
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //返回json
        response.setContentType("application/json;charset=utf-8");
        String data = "{\"code\":200, \"msg\":\"登出成功\", \"data\":{}}";
        response.getWriter().write(data);
    }
}

配置登出。

//用户登出控制
http.logout()
    .logoutSuccessHandler(new MyLogoutSuccessHandler())

二、用户授权

Spring Security 登录成功之后第二步就授权,所以要先学习一下 Spring Security中是怎么进行用户授权的,学习之前,先了解一下基于角色的权限管理,它有助于理解Spring Security 中的用户授权。

2.1、RBAC概念

基于角色的访问控制(Role-Based Access Control,简称RBAC),主要是通过将权限分配给角色,再将角色分配给用户,简化权限管理的复杂性。RBAC权限管理是我们常见的一种方式。

  • 用户(User):指的是系统中的用户,每个用户可以被分配一个或者多个角色。
  • 角色(Role):指的是权限的集合,一个角色包含多种操作权限。
  • 权限(Permission):指的是对系统资源进行特定的操作,比如用户增加、用户修改等。

实现原理:Javaweb 中权限的控制,其实就是对请求映射方法控制,如果登录用户有这个权限,允许访问,如果没有权限,不允许访问。

基于这个原理,RBAC实现步骤如下

  1. 自定义一个权限注解:@PermissionAnno,约定方法上有这个注解的,必须进行权限校验。
  2. 在需要进行权限校验的请求映射方法中增加这个注解。
  3. 使用拦截器对所有请求进行拦截,每次访问进行权限校验。
  4. 如果使用admin登录,不需要拦截。

Spring Security 支持RBAC授权,所以授权原理是一样的。具体代码实现方案有2种,一种为配置方式,一种为注解方式。

2.2、基于配置方式授权

权限配置授权以用户CURD操作为例进行学习。

步骤1: 定义用户CURD4个接口

package com.duan.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author db
 * @version 1.0
 * @description UserController
 * @since 2024/9/2
 */
@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping("/insert")
    public String insert(){
        return "user-insert";
    }
    @GetMapping("/update")
    public String update(){
        return "user-update";
    }
    @GetMapping("/delete")
    public String delete(){
        return "user-delete";
    }
    @GetMapping("/list")
    public String list(){
        return "user-list";
    }
}

步骤2: 在项目SecurityConfig中配置4个接口访问权限。

        // 请求url权限控制
        http.authorizeRequests()
                .antMatchers("/user/insert").hasAuthority("user:insert")
                .antMatchers("/user/update").hasAuthority("user:update")
                .antMatchers("/user/delete").hasAuthority("user:delete")
                .antMatchers("/user/list").hasAuthority("user:list")
                .anyRequest().authenticated();
// 表示访问/user/list接口只需要有user:list或者user:query权限即可
.antMatchers("/user/list").hasAnyAuthority("user:list", "user:query")

步骤3: 配置用户权限在项目SecurityConfig中配置用户权限,cxykk用户有"user:insert", "user:update"权限。

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).authorities("user:insert", "user:update").build());
        manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).authorities("p2").build());
        return manager;
    }

步骤4: 测试权限校验前需要先登录,使用cxykk用户登录, 登录成功后访问下面2组接口。

访问http://localhost:8183/user/delete和http://localhost:8183/user/delete报错。

角色配置授权

以用户CURD操作为例进行学习。

步骤1: 定义用户CURD4个接口。

package com.duan.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author db
 * @version 1.0
 * @description UserController
 * @since 2024/9/2
 */
@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping("/insert")
    public String insert(){
        return "user-insert";
    }
    @GetMapping("/update")
    public String update(){
        return "user-update";
    }
    @GetMapping("/delete")
    public String delete(){
        return "user-delete";
    }
    @GetMapping("/list")
    public String list(){
        return "user-list";
    }
}

步骤2: 4个接口运行访问的配置。

        // 请求url权限控制
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/fail").permitAll()
                .antMatchers("/logoutSuccess").permitAll()
//                .antMatchers("/user/insert").hasAuthority("user:insert")
//                .antMatchers("/user/update").hasAuthority("user:update")
//                .antMatchers("/user/delete").hasAuthority("user:delete")
//                .antMatchers("/user/list").hasAuthority("user:list")
                .antMatchers("/user/insert").hasRole("user_mag")
                .antMatchers("/user/update").hasRole("user_mag")
                .antMatchers("/user/delete").hasAuthority("hr")
                .antMatchers("/user/list").hasAuthority("hr")
                .anyRequest().authenticated();
// 表示访问 **"/user/list"**  接口只需要拥有 **"user_mag"** 或 **"hr"**角色
.antMatchers("/user/list").hasAnyRole("user_mag", "hr")

步骤3: 配置用户权限

在项目SecurityConfig中配置用户权限,cxykk用户有"user:insert", "user:update"权限。

步骤4: 测试

权限校验前需要先登录,使用cxykk用户登录, 登录成功后访问下面2组接口。

访问http://localhost:8183/user/deletehttp://localhost:8183/user/delete报错。

2.3、基于注解方式授权

上面的配置方式比较简单,开发中很少使用,当接口多了之后,一个个配置非常麻烦,Spring Security2.X中存在注解方式,简化操作配置。

Spring Security 注解方式授权与鉴权涉及到2个核心的注解:@PreAuthorize @Secured其中@PreAuthorize 是基于权限授权(也可以基于角色),@Secured 是基于角色授权,先看基于权限授权。权限注解方式以用户CURD 操作为例进行学习。

步骤1、 配置启动注解

默认情况下Spring Security是不支持注解鉴权的,此时需要配置类中开启。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig  extends WebSecurityConfigurerAdapter {
    ...
}

步骤2: 定义用户CURD4个接口注意:使用注解标记方法接口需要的权限。

package com.duan.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author db
 * @version 1.0
 * @description UserController
 * @since 2024/9/2
 */
@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping("/insert")
    @PreAuthorize("hasAnyAuthority('user:insert')")
    public String insert(){
        return "user-insert";
    }
    @GetMapping("/update")
    @PreAuthorize("hasAnyAuthority('user:update')")
    public String update(){
        return "user-update";
    }
    
    @GetMapping("/delete")
    @PreAuthorize("hasAnyAuthority('user:delete')")
    public String delete(){
        return "user-delete";
    }
    
    
    @GetMapping("/list")
    @PreAuthorize("hasAnyAuthority('user:list')")
    public String list(){
        return "user-list";
    }
}

步骤3: 配置用户拥有的权限。

    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).authorities("user:insert", "user:update").build());
        manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).authorities("p2").build());
        return manager;
    }

步骤4: 移除http.authorizeRequests中权限配置。

        // 请求url权限控制
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/fail").permitAll()
                .antMatchers("/logoutSuccess").permitAll()
//                .antMatchers("/user/insert").hasAuthority("user:insert")
//                .antMatchers("/user/update").hasAuthority("user:update")
//                .antMatchers("/user/delete").hasAuthority("user:delete")
//                .antMatchers("/user/list").hasAuthority("user:list")
                .anyRequest().authenticated();

步骤5: 测试

权限校验前需要先登录,使用cxykk用户登录, 登录成功后访问下面2组接口。

角色注解授权

需求: 以用户CRUD 操作为例子-注解-角色

步骤1: 配置启动注解

默认情况下Spring Security是不支持注解鉴权的,此时需要配置类中开启。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig  extends WebSecurityConfigurerAdapter {
    ...
}

步骤2:定义用户crud4个接口注意:使用注解标记方法接口需要的权限。

package com.duan.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author db
 * @version 1.0
 * @description UserController
 * @since 2024/9/2
 */
@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping("/insert")
//    @PreAuthorize("hasAnyAuthority('user:insert')")
    @PreAuthorize("hasAnyRole('user_mag')")
    public String insert(){
        return "user-insert";
    }
    @GetMapping("/update")
//    @PreAuthorize("hasAnyAuthority('user:update')")
    @PreAuthorize("hasAnyRole('user_mag')")
    public String update(){
        return "user-update";
    }

    @GetMapping("/delete")
//    @PreAuthorize("hasAnyAuthority('user:delete')")
    @PreAuthorize("hasAnyRole('hr')")
    public String delete(){
        return "user-delete";
    }


    @GetMapping("/list")
//    @PreAuthorize("hasAnyAuthority('user:list')")
    @PreAuthorize("hasAnyRole('hr')")
    public String list(){
        return "user-list";
    }
}

步骤3: 配置用户拥有的权限。

    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).roles("user_mag").build());
        manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).roles("p2").build());
        return manager;
    }

步骤4: 移除http.authorizeRequests中权限配置。

        // 请求url权限控制
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/fail").permitAll()
                .antMatchers("/logoutSuccess").permitAll()
//                .antMatchers("/user/insert").hasRole("user_mag")
//                .antMatchers("/user/update").hasRole("user_mag")
//                .antMatchers("/user/delete").hasAuthority("hr")
//                .antMatchers("/user/list").hasAuthority("hr")
                .anyRequest().authenticated();

步骤5: 测试

权限校验前需要先登录,使用cxykk用户登录, 登录成功后访问下面2组接口。

@Secured 注解也是可以授权的,只不过是它不支持SpEL表达式,只能指定特定角色,如果用户没有满足注解内指定的角色之一,方法调用会被拒绝。在这里就不细讲了。

三、权限异常处理

当授权出现异常时,默认返回的是403页面,可以通过exceptionHandling 进行控制。

页面跳转

步骤: 定义nopermission.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org/" lang="en">
  <head>
    <meta charset="UTF-8">
    <title>没有权限</title>
  </head>
  <body>
    <h1>没有权限</h1>
  </body>
</html>

步骤2: 配置权限异常跳转页面。

//异常控制
http.exceptionHandling()
    .accessDeniedPage("/nopermission.html");

Json格式返回

步骤1: 定义权限异常处理器。

package com.duan.handler;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author db
 * @version 1.0
 * @description MyAccessDeniedHandler
 * @since 2024/9/5
 */
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //返回json
        response.setContentType("application/json;charset=utf-8");
        String data = "{\"code\":403, \"msg\":\"没有权限\", \"data\":{\"error\":"+accessDeniedException.getMessage()+"}}";
        response.getWriter().write(data);
    }
}

步骤2: 配置权限异常处理器。

//异常控制
http.exceptionHandling()
    .accessDeniedHandler(new MyAccessDeniedHandler())

代码地址:https://gitee.com/duan138/practice-code/tree/dev/springsecurity

四、总结

通过配置方式去加深了解Spring Security中用户认证和用户授权,虽然在项目中不是通过这种配置方式去进行编码,都是从数据库中进行用户认证和授权,但是了解这些之后,再去通过数据库去进行认证和授权,将会简单很多,先了解基础,然后进行实践。

下篇文章将要进行实战,通过SpringBoot + Spring Security + JWT + Mybaties-plus + Redis 进行数据库级别的认证和授权。


改变你能改变的,接受你不能改变的,关注公众号:程序员康康,一起成长,共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值