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 如何使用数据库账号密码进行认证
- 实现UserDetailsService接口,重写loadUserByUsername方法
- 创建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
- 方案一:定制JWT过滤器,嵌入SpringSecurity 过滤链体系
- 方案二:重写认证处理器DaoAuthenticationProvider的additionalAuthenticationChecks方法
方案一实现思路:
- 创建配置类(禁用会话,密码匹配器,csrf,放行接口,登陆成功逻辑,登陆失败逻辑,将自定义过滤器添加到过滤器链中)
- 自定义过滤器(继承OncePerRequestFilter,重写doFilterInternal)
- 控制器实现登录接口
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());
}
});
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?