• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
打工人丶
博客园    首页    新随笔    联系   管理    订阅  订阅

SpringSecurity入门详解

1. SpringSecurity简介

https://space.bilibili.com/494956170/?spm_id_from=333.999.0.0
SpringSecurity:是一个高度自定义的安全框架,利用 Spring loC、DI 和 AOP 功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大星重复代码的工作。

本质:是一个过滤器链,由多个过滤器组成。







2. 快速入门

2.1 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.2 验证

@RestController
@RequestMapping(value ="/index")
public class IndexController {
	
	@GetMapping(value = "/toIndex")
	public MsgResult toIndex(){
		return MsgResult.success("欢迎来到index");
	}
}

2.3 现象

导入了SpringSecurity依赖后,SpringBoot对其有很好的整合,就导入个依赖,springsecurity就帮我们管理权限了。
账号:user
密码:控制台已经输出

发现的问题:

问题1. springsecurity帮我们做了一个登录页,实际上我们可能不需要他提供的登录页,所以我们需要对其改造。

问题2. springsecuriity在 userDetailService 环节默认的使用的是基于内存实现的数据库认证。实际上我们也不会使用该种方式,而回采用数据库的方式,所以也需要对其进行改造。






3. 探究认证功能

3.1 SpringSecurity本质

SpringSecurity本质:其实就是一个过滤器链,内部提供了各种功能的过滤器。springsecurity底层就是这些过滤器一层一层的执行帮我们实现的权限管理。


具体实现原理涉及到的接口:

  所涉及的类比较复杂,我们只讲解最核心的。

  总之就是我们的web项目里面他有一个自己的filter过滤器链(FilterChain,里面就是它的各种Filter),其中有个Spring提供了一个
DelegatingFilterProxy类,允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。

  而SpringSecurity底层就借助了它,让其调用SpringSecurity的FilterChainProxy这个代理类,而他会调用SecirityFilterChain这个过滤器链接口(常用实现类:DefaultSecurityFilterChin,而这个实现类里面就加载了security提供的16个Filter)


DefaultSecurityFilterChin类:

    该类是 SecirityFilterChain 的实现类,加载了默认的16个Filter。
    这16个Filter就是Security提供的,用来帮助实现登录,注销,授权,跨域等操作的。



3.2 SpringSecurity认证原理

SpringSecurity的简要认证流程:security提供了16个过滤器以完成它的各种功能,下图则是它认证的核心过滤器





3.3 SpringSecurity 如何使用数据库账号密码进行认证

  1. 实现UserDetailsService接口,重写loadUserByUsername方法
  2. 创建security配置类

前言:想要使用我们的数据库账号密码来做认证,就要改写更改 SpringSecurity 的认证逻辑。

3.2.1 SpringSecurity 实现UserDetailsService接口,重写loadUserByUsername方法的认证逻辑
/**
 * @author lihao
 * @version 1.0
 * @ClassName SecurityUserServiceImpl
 * @date 2023/5/25  9:43
 * @apiNote 
        UserDetailsService接口(由springsecurity提供):
                  用于加载用户的详细信息,以供身份验证和授权使用。它提供了一个方法loadUserByUsername(String username)用于用户名获取用户的详细信息。
 *                我们想更改springcurity的认证,我们就可以实现UserDetailsService,因为security底层就是用其做核心认证的。
 **/
@Service
public class SecurityUserServiceImpl extends ServiceImpl<SecurityUserMapper, SecurityUser> implements SecurityUserService, UserDetailsService {
	
	@Resource
	private SecurityUserMapper securityUserMapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名查询用户,判断用户数是否存在
		LambdaQueryWrapper<SecurityUser> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(SecurityUser::getUsername,username);
		SecurityUser user = securityUserMapper.selectOne(wrapper);
		if (user==null) {
			throw new UsernameNotFoundException("用户没找到"); // 这个异常是springsecurity提供的,如果抛这个异常,他就会捕获,表示认证失败。你也可以不用他这个
		}

        // 2. 创建权限集合
		List<GrantedAuthority> adminAuthority = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
		
        // 3. 返回 UserDetails
        // loadUserByUsername(String username)方法需要返回一个UserDetails对象,它是一个接口,所以要么返回它的实现类,要么你创建一个它的实现类返回。 这里我们用它提供的实现类 User。
        // 该User对象需要三个参数。(可以查看它的源码,其实他有2个构造参数,另一个就是所有属性让你自填,他有好几个属性,我们一般用不到。就用这个有三个参数的构造函数就行。)
        // springsecurity会将你在构造参数传的账号密码(来自你数据库的账号密码)跟在登录页提供过来的账号密码做匹配校验判断是否登录成功
        // 如果账号密码匹配失败,默认会重定向到 /login?error去
		//      参数1:用户名
		//      参数2:密码      (SpringSecurity强制要求加密,他要求在密码前面加一个前缀 {noop} ),要么你配置加密组件,否则需要加密:new BCryptPasswordEncoder().encode(user.getPassword())  ---- 可以这么写
		//      参数3:权限集合   这里只做认证,所以随便搞一个角色集合,不影响认证,注意,这里第三个参数不能直接传null,必须给它一个集合。(源码里面,关于该属性注释上有说明不能为空)
		return new User(user.getUsername(), user.getPassword(), adminAuthority); // 在下方,我们会配置加密组件,所以不需要对这里的密码加密。(否则,你的密码前面要加一个标识符)
	}
}
3.2.2 编写security配置类,配置加密组件

/**
 * @author lihao
 * @version 1.0
 * @ClassName SecurityConfig
 * @date 2023/5/25  9:03
 * @apiNote 
 *          WebSecurityConfigurerAdapter:
 *              继承该类是为了通过重写SpringBoot对SpringSecurity的默认配置。
 *              它提供了一些方法,您可以重写这些方法来定义安全规则、配置身份验证和授权方式,以及定制其他与安全相关的行为。
 *
 *          常用方法:(我们重写这些方法,就能调整security的默认配置)
 *              configure(HttpSecurity http): 这是最重要的方法之一,用于配置如何通过拦截器保护HTTP请求。您可以定义哪些URL路径需要特定的安全配置,例如要求身份验证、授权规则和访问权限等。
 *
 *              configure(AuthenticationManagerBuilder auth): 这个方法用于配置身份验证机制。您可以定义用户存储的位置、身份验证规则以及密码编码器等。
 *
 *              configure(WebSecurity web): 这个方法用于配置Spring Security忽略特定的静态资源,例如CSS、JavaScript文件或其他不需要身份验证的静态资源。
 *
 *              userDetailsService(): 这个方法返回一个UserDetailsService对象,用于从数据库或其他数据源加载用户信息。
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Resource
	private UserDetailsService userDetailsService;
	
	// 因为springsecurity底层做认证的时候 必须 用到密码匹配器。所以必须配一下。
    // PasswordEncoder 是一个接口,BCryptPasswordEncoder是其其中的一个实现类。(也是springsecurity推荐使用的该实现类)
	@Bean
	public PasswordEncoder passwordEncoder(){
		return new BCryptPasswordEncoder();
	}
	
    // configure(AuthenticationManagerBuilder auth): 这个方法用于配置身份验证机制。您可以定义用户存储的位置、身份验证规则以及 【密码编码器】 等。
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // AuthenticationManagerBuilder常用方法:
                // userDetailsService: 这个方法接受一个UserDetailsService对象,用于加载用户的详细信息。UserDetailsService是一个接口,您需要实现它来根据用户名加载用户信息。

                // inMemoryAuthentication: 这个方法允许您在内存中配置用户详细信息。您可以指定用户名、密码和用户角色。这对于快速测试和开发目的非常方便。

                // jdbcAuthentication: 这个方法允许您使用JDBC来加载用户详细信息。您需要提供一个DataSource对象和相应的查询语句来检索用户名、密码和角色信息。

                // ldapAuthentication: 这个方法用于配置LDAP(轻量级目录访问协议)身份验证。您需要提供LDAP服务器的连接信息和相应的查询语句。

                // authenticationProvider: 这个方法允许您提供自定义的AuthenticationProvider实现,用于验证用户的身份。

		// userDetailsService(T userDetailsService):    根据传入的自定义UserDetailsService做身份验证。
		// 返回值:会返回一个DaoAuthenticationConfigurer(该类继承AbstractDaoAuthenticationConfigurer抽象类)}
        DaoAuthenticationConfigurer<AuthenticationManagerBuilder, UserDetailsService> detailsService = auth.userDetailsService(userDetailsService);

        // passwordEncoder(PasswordEncoder passwordEncoder): 来自于抽象类, 用于指定自定义密码匹配器,便于后续springsecutiry底层后续使用。
		detailsService.passwordEncoder(passwordEncoder());
	}
}
    此时登录,就会使用你数据库的账号密码来做认证。



3.4 SpringSecurity 如何更改登录页

3.3.1 编写security配置类,重写 void configure(HttpSecurity http) 方法中springsecurity默认的HTTP请求拦截规则

/**
 * @author lihao
 * @version 1.0
 * @ClassName SecurityConfig
 * @date 2023/5/25  9:03
 * @apiNote WebSecurityConfigurerAdapter:
 *              继承该类是为了通过重写其方法来自定义和配置Spring Security的行为。
 *              它提供了一组方法,您可以重写这些方法来定义安全规则、配置身份验证和授权方式,以及定制其他与安全相关的行为。
 * 
 *          常用方法:(我们重写这些方法,就能调整security的默认配置)
 *              configure(HttpSecurity http): 这是最重要的方法之一,用于配置如何通过拦截器保护HTTP请求。您可以定义哪些URL路径需要特定的安全配置,例如要求身份验证、授权规则和访问权限等。

 *              configure(AuthenticationManagerBuilder auth): 这个方法用于配置身份验证机制。您可以定义用户存储的位置、身份验证规则以及密码编码器等。

 *              configure(WebSecurity web): 这个方法用于配置Spring Security忽略特定的静态资源,例如CSS、JavaScript文件或其他不需要身份验证的静态资源。

 *              userDetailsService(): 这个方法返回一个UserDetailsService对象,用于从数据库或其他数据源加载用户信息。
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Resource
	private UserDetailsService userDetailsService;
	
	.
	. 代码和上面配置数据库认证一样,这里不在赘述
	.
	
	// 参数HttpSecurity常用方法:
		// formLogin():配置表单登录认证方式。
		// authorizeRequests():配置对请求进行授权的方式。
		// antMatchers(String... antPatterns):指定需要受保护的 URL 或路径模式。
		// authenticated():要求进行身份验证。
		// hasAnyRole(String... authorities):指定允许访问资源的角色或权限。
		// permitAll():无条件允许访问资源。
		// logout():配置退出登录认证方式。
		// csrf():启用或禁用 CSRF 保护机制。
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// http.formLogin() 是Spring Security中用于配置基于表单的身份验证的方法。
		// 常用方法如下:(还有很多方法,自己写的时候通过 .方法 查看)
				// loginPage(String loginPage):                                  指定登录页面的URL。如果不指定,则默认为/login
				// loginProcessingUrl(String loginProcessingUrl):                指定登录表单提交的URL。默认为/login(也就是我们的Controller地址)
				// usernameParameter(String usernameParameter):                  指定登录表单中用户名字段的参数名。默认为username,如果你的表单提交的用户名密码不是这个,你就需要来特地指定
				// passwordParameter(String passwordParameter):                  指定登录表单中密码字段的参数名。默认为password,如果你的表单提交的用户名密码不是这个,你就需要来特地指定
				// defaultSuccessUrl:                                            登录成功后的访问地址,注意:该方法只有当用户从登录页登录的时候才生效。如果用户没登陆,去访问一个需要登录后才能访问的页面,导致被跳转到登录页,此时从登录页登录进来就不生效了。
				// successForwardUrl:                                            登录成功后的转发地址,全局生效,他就能解决上面那个问题。
				// successForwardHandler:                                        登陆成功的处理逻辑,需要传一个Handler对象(这个方法允许开发人员使用自定义的 AuthenticationSuccessHandler 来处理用户成功登录后的行为,自定义类实现相关接口,重写方法。)
				// failureUrl(String failureUrl):                                指定登录失败后的跳转页面。默认为/login?error
				// failureHandler(AuthenticationFailureHandler failureHandler):  指定自定义的登录失败处理器
				// successForwardUrl(String successUrl):                         指定登录成功后的跳转页面。默认为/login?error
				// successHandler(AuthenticationSuccessHandler successHandler):  指定自定义的登录成功处理器
				// and():                                                        用于连接其他配置方法
				// permitAll():                                                  允许所有用户访问登录页面和登录处理URL
		http.formLogin()
			.loginPage("/login.html")
			.loginProcessingUrl("/login")//指定登录访问路径(登录页面的form表单提交路径,注意必须一致,我的login.html里面写的提交路径是/login,所以这里我也写/login)
			.defaultSuccessUrl("/index.html")
			.failureUrl("/error.html");//指定用户名或密码错误访问的页面
		
		// http.authorizeRequests() 是Spring Security中用于配置请求权限的方法。
		// 常用方法如下:(还有很多方法,自己写的时候通过 .方法 查看)
				// antMatchers(String... antPatterns):                            指定要进行安全配置的URL模式。可以使用Ant风格的通配符,例如/admin/**
				// regexMatchers(String... regexPatterns):                        使用正则表达式指定要进行安全配置的URL模式
				// mvcMatchers(String... mvcPatterns):                            指定要进行安全配置的MVC URL模式
				// requestMatchers(RequestMatcher... requestMatchers):            指定自定义的请求匹配器。
				// anyRequest():                                                  匹配任何请求,用于配置对所有请求的安全设置
				// authenticated():                                               要求用户在访问受保护的URL时进行身份验证
				// permitAll():                                                   允许所有用户访问受保护的URL,无需进行身份验证
				// denyAll():                                                     拒绝所有用户访问受保护的URL
				
				// hasRole(String role):                                          要求用户具有指定角色才能访问受保护的URL
				// hasAnyRole(String... roles):                                   要求用户具有指定角色之一才能访问受保护的URL
				// hasAuthority(String authority):                                要求用户具有指定权限才能访问受保护的URL
				// hasAnyAuthority(String... authorities):                        要求用户具有指定权限之一才能访问受保护的URL
				// access(String accessExpression):                               使用SpEL表达式指定访问受保护的URL的条件
		http.authorizeRequests()
			//.antMatchers("/index/testRoleAndPermission").hasAnyRole("admin")//设置哪些路径需要什么权限
			//.antMatchers("/index/anyOne","/index/anyTwo").permitAll()//设置哪些路径可以直接访问,不需要认证
			//.anyRequest().authenticated();//其余任何请求都需要认证
			.antMatchers("/login.html").permitAll() // 放行login.html
			.antMatchers("/error.html").permitAll() // 放行login.html
			.antMatchers("/login").permitAll() // 放行login
			.anyRequest().authenticated(); // 其余请求必须认证才能访问
		
		// http.logout()是Spring Security中用于配置用户注销功能的方法。
		// 常用方法如下:(还有很多方法,自己写的时候通过 .方法 查看)
			// logoutUrl(String logoutUrl):                                        指定注销的URL,默认为"/logout"。
			// logoutSuccessUrl(String logoutSuccessUrl):                          指定注销成功后的跳转页面。
			// invalidateHttpSession(boolean invalidate):                          设置在注销时是否使HttpSession失效,默认为true。
			// deleteCookies(String... cookieNames):                               指定在注销时要删除的cookie名称。
		//http.logout()
			//.logoutUrl("/logout")//注销接口
			//.logoutSuccessUrl("/needLogin").permitAll();//注销成功后的跳转路径
		
		// http.exceptionHandling()是Spring Security中用于配置异常处理的方法。
		// 常用方法如下:(还有很多方法,自己写的时候通过 .方法 查看)
			// accessDeniedPage(String accessDeniedUrl):                                          指定访问被拒绝时重定向的页面。在用户访问被拒绝时,将会重定向到指定的页面。
			// authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint):       指定在认证失败时的处理策略。可以自定义认证失败时的处理方式,例如返回特定的错误信息或执行特定的操作。
			// defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint, RequestMatcher requestMatcher):为特定的请求配置默认的认证失败处理策略。可以为特定的请求定义不同的认证失败处理策略,使得针对不同请求的认证失败有不同的处理方式。
		//http.exceptionHandling()
		//    .accessDeniedPage("/noPermission.html");//没有权限的跳转页面
		
		http.csrf()
			.disable();//关闭csrf防护
	}
}



3.5 前后端分离模式下——登陆成功逻辑处理

前面我们定制登录成功后使用successForwardUrl 跳转某个接口路径,如果是前后端分离项目,要求返回是json格式,那怎么办?又比如,跳转都某个路径前需要执行某个逻辑,这该怎么办?此时可以使用登录成功处理器

    //用户登录控制
    http.formLogin()
        .successHandler(new MyAuthenticationSuccessHandler()); // 配置登录成功后的处理器

    public class MyAuthenticationSuccessHandler  implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {

            //response.sendRedirect("/要跳转的路径");
            
            //返回json
            response.setContentType("application/json;charset=utf-8");
            String data = "{\"code\":200, \"msg\":\"登录成功\", \"data\":{}}";
            response.getWriter().write(data);

        }
    }

注意:如果也配置successForwardUrl ,以后面配置的为主,也就是后面覆盖前面

    http.formLogin()
        .successHandler(new MyAuthenticationSuccessHandler())
        .successForwardUrl("/success")



3.6 前后端分离模式下——登陆失败逻辑处理

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

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //response.sendRedirect("/要跳转的路径");

        //返回json
        response.setContentType("application/json;charset=utf-8");
        String data = "{\"code\":500, \"msg\":\"登录失败\", \"data\":{\"error\":"+exception.getMessage()+"}}";
        response.getWriter().write(data);
    }
}

注意:如果也配置failureForwardUrl,以后面配置的为主,也就是后面覆盖前面

    http.formLogin()
        .failureHandler(new MyAuthenticationFailureHandler())
        .failureForwardUrl("/fail")






4. 用户注销与会话控制功能

4.0 用户注销

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //注销的配置
        http.logout()
                .logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

        http.authorizeRequests()
            .antMatchers("/logout").permitAll() //注销时访问的路径
            .antMatchers("/logoutSuccess").permitAll()
            .anyRequest().authenticated();



4.1 前后端分离模式下——用户注销逻辑处理

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

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



4.2 会话管理——获取当前登录用户

SpringSecurity提供了会话管理,认证通过后,将身份信息放入SecurityContextHolder上下文中。

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    Object principal = authentication.getPrincipal();



4.3 会话控制——前后端分离下的选择

Security提供了四种会话控制:

* always        如果没有session存在就创建一个。
* ifRequired    如果需要就创建一个Session(默认)。
* never         将不会创建Session,但如果应用中其他地方创建了session,就会使用那个session。
* stateless     将绝不会创建Session,也不会使用Session    前后端分离下选择,选择该种会话后,将不再保存登录会话信息,需要我们自己处理(存入redis,token等方法解决)

设置会话

    http.sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);






5. 角色与权限

前言:本文只介绍基于注解的方式实现授权功能。

Spring Security提供了四个方法用于角色和权限的访问控制:

* hasAuthority        判断当前主体是否有指定的权限,有返回true,否则返回false。该方法适用于只拥有一个权限的用户。
* hasAnyAuthority     适用于一个主体有多个权限的情况,多个权限用逗号隔开。
* hasRole             如果用户具备给定角色就允许访问,否则报403错误。
* hasAnyRole          设置多个角色,多个角色之间使用逗号隔开,只要用户具有某一个角色,就能访问。

5.1 开启基于注解的授权功能

@SpringBootApplication
@MapperScan("com.lihao.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) // 开启SpringSecurity注解功能,securedEnabled = true是开启@Secured的支持, prePostEnabled = true是开启@PreAuthorize @PostAuthorize的支持
public class SecurityLeanApplication {
	public static void main(String[] args) {
		SpringApplication.run(SecurityLeanApplication.class, args);
	}
}

5.2 授权注解介绍

    @Secured:判断是否具有角色,需要注意的是:匹配字符串需要添加前缀“ROLE_”。

    @PreAuthorize:方法执行前进行权限验证。

    @PostAuthorize:方法执行后进行权限验证,适合验证带有返回值的权限。



5.3 实操

    @SpringBootApplication
    @MapperScan("com.lihao.mapper")
    @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) // 开启SpringSecurity注解功能,securedEnabled = true是开启@Secured的支持, prePostEnabled = true是开启@PreAuthorize @PostAuthorize的支持
    public class SecurityLeanApplication {
    	public static void main(String[] args) {
    		SpringApplication.run(SecurityLeanApplication.class, args);
    	}
    }
	@Override
	public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
		// 1. 判断用户是否存在
		LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(User::getUsername, userName);
		User user = userMapper.selectOne(wrapper);
		if (user == null) {
			throw new RuntimeException("用户名或密码错误");
		}
		
		// 2. 获取用户角色信息
		List<String> roleNameList = roleMapper.getRoleNameByUserId(user.getId());
		StringBuilder roleName = new StringBuilder();
		StringBuilder menuCode = new StringBuilder();
		if (!CollectionUtils.isEmpty(roleNameList)) {
			for (String s : roleNameList) {
				if (roleName.length() > 0) {
					roleName.append(",");
				}
				roleName.append("ROLE_").append(s);
				
				
				// 3. 获取权限信息(规定了角色名不能重复)
				List<String> menuCodeList = menuMapper.getMenuCodeByRoleName(s);
				if (!CollectionUtils.isEmpty(menuCodeList)) {
					for (String menu : menuCodeList) {
						if (menuCode.length() > 0) {
							menuCode.append(",");
						}
						menuCode.append(menu);
					}
				}
			}
		}
		List<GrantedAuthority> adminAuthority = AuthorityUtils.commaSeparatedStringToAuthorityList(roleName + "," + menuCode);
		log.info(roleName + "," + menuCode);
		
		return new org.springframework.security.core.userdetails.User(
			user.getUsername(),
			user.getPassword(),
			adminAuthority);
	}
	@GetMapping("/needAuthority")
	@PreAuthorize("hasAuthority('user:select')")
	public List<User> needAuthority(){
		return userService.list();
	}



5.4 前后端分离模式下——权限异常处理

    //异常控制
    http.exceptionHandling()
        .accessDeniedHandler(new MyAccessDeniedHandler())
    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);
        }
    }






6. JWT

JWT:一种加密后的数据载体,阔以在各应用之间进行数据传输。

一个JWT由三部分组成,HEADER,RAYLOAD,SIGNATURE。三者之间使用 "." 连接。


6.1 Header头部

头部承载两部分信息:

  • 声明类型,默认JWT
  • 声明加密的算法,常见的算法:HMAC,RSA,ECDSA等
    {
      "alg": "HS256",
      "typ": "JWT"
    }

    alg:表示签名的算法。
    typ:表示令牌的类型。

将其使用Base64加密,构成了JWT第一部分 header。 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9


6.2 Payload组成

Payload是一个JSON对象,用来存放实际需要传递的有效信息。他里面是 key-value 形式的。

它有一些是自带的属性,你也可以自定义属性。

自带的:

  • iss 签发人
  • iat 签发时间
  • exp 过期时间(必须大于签发时间)
  • sub 主体(用来做什么)
  • aud 受众(给谁用的),比如:www.xxx.com
  • nbf 生效时间
  • jti 编号,JWT的唯一身份标识

自定义:

  • 你可以添加任何信息,不推荐添加敏感数据,因为该内容阔以被解密。

使用base64对其编码,就构成了JWT的第二部分 payload。


6.3 signature组成

signature:是对前两部分的签名,防止数据篡改。

首先,你需要指定一个密钥(secret),这个密钥只有服务器才知道,不能泄露给用户。然后使用Header里面指定的签名算法,按照指定的公式产生签名。

得到签名后使用Base64对其加密,构成JWT第三部分 signature。







7. 生成、解析JWT

7.1 引入依赖

    <!--引入jwt-->
    <dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.4.0</version>
    </dependency>

7.2 生成token

    //生成令牌
    @Test
    public void testCreate(){
        String token = JWT.create()
            .withClaim("username", "dafei")//设置自定义用户名
            .sign(Algorithm.HMAC256("abcdefghijklmnopqrstuvwxyz"));//设置签名 保密 复杂

        System.out.println(token);
    }


    //设置有过期的令牌
    @Test
    public void testExpired() throws InterruptedException {
        String token = JWT.create()
            .withClaim("username", "dafei")//设置自定义用户名
            .withExpiresAt(new Date(System.currentTimeMillis() + 5 * 1000L))   //5s中
            .sign(Algorithm.HMAC256("abcdefghijklmnopqrstuvwxyz"));//设置签名 保密 复杂

        System.out.println(token);
    }

    //解析令牌
    @Test
    public void testParse(){
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsicGhvbmUiLCIxNDMyMzIzNDEzNCJdLCJleHAiOjE1OTU3Mzk0NDIsInVzZXJuYW1lIjoi5byg5LiJIn0.aHmE3RNqvAjFr_dvyn_sD2VJ46P7EGiS5OBMO_TI5jg";

        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("abcdefghijklmnopqrstuvwxyz")).build();

        DecodedJWT decodedJWT = jwtVerifier.verify(token);

        System.out.println("用户名: " + decodedJWT.getClaim("username").asString());
    }

7.3 token认证逻辑

优点:

  • 简洁: 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
  • 自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。






8. SpringSecurity集成JWT

https://www.bilibili.com/video/BV13w411s7fw/?spm_id_from=333.999.top_right_bar_window_history.content.click&vd_source=61b6fb4e547748656e36b17ee95125fb
方法:

  1. 方案一:定制JWT过滤器,嵌入SpringSecurity 过滤链体系
  2. 方案二:重写认证处理器DaoAuthenticationProvider的additionalAuthenticationChecks方法

方案一实现思路:

  1. 创建配置类(禁用会话,密码匹配器,csrf,放行接口,登陆成功逻辑,登陆失败逻辑,将自定义过滤器添加到过滤器链中)
  2. 自定义过滤器(继承OncePerRequestFilter,重写doFilterInternal)
  3. 控制器实现登录接口

8.1 security配置类

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

	@Resource
	private UserDetailsService userDetailsService;

        //密码加密器
	@Bean
	public PasswordEncoder passwordEncoder(){
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// authenticationProvider(): 这个方法允许您提供自定义的AuthenticationProvider实现,用于验证用户的身份。
		DaoAuthenticationConfigurer<AuthenticationManagerBuilder, UserDetailsService> detailsService = auth.userDetailsService(userDetailsService);
		detailsService.passwordEncoder(passwordEncoder());
	}

        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }

        //自定义配置
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //禁用csrf保护-前后端分离项目需要禁用,认证鉴权项目也要需要禁用
            http.csrf().disable();

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

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


            //会话控制
            http.sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

            //过滤器控制,将其放在UsernamePsswordAuthenticationFilter之前,一旦jwt校验通过之后,用户状态转为认证成功状态,那么UsernamePasswordAuthenticationFilter 就不会再执行校验逻辑了。
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        }
}

8.2 定制jwt检验过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {

        httpServletResponse.setContentType("application/json;charset=utf-8");

        // 放行一些非拦截接口请求
        String url = httpServletRequest.getRequestURI();
        if(url.startsWith("/jwt/login")){
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        // 检验jwt 是否为空,时效性
        String token = httpServletRequest.getHeader("token");
        if(!StringUtils.hasText(token)){
            httpServletResponse.getWriter().write("token 不能为空");
            return;
        }
        if(!JWTUtils.isExpired(token)){
            httpServletResponse.getWriter().write("token 失效");
            return;
        }

        String username = JWTUtils.getToken(token, "username");
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null){
            httpServletResponse.getWriter().write("token校验失败,请重新登录");
            return;
        }

        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

8.3 controller

@RestController
public class LoginController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/jwt/login")
    public String login(String uname, String pwd){
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uname,pwd);
        Authentication authenticate = authenticationManager.authenticate(token);
        if(authenticate != null && authenticate.isAuthenticated()){
            //登录成功
            return JWTUtils.createToken("username", uname);
        }
        throw new RuntimeException("账号与密码出错");
    }
}

8.4 配置认证异常/鉴权异常

//异常控制
http.exceptionHandling()
    .accessDeniedHandler(new MyAccessDeniedHandler())
    .authenticationEntryPoint(new AuthenticationEntryPoint() {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("has error:" + authException.getMessage());
        }
    });
posted @ 2023-05-25 00:28  &emsp;不将就鸭  阅读(418)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3