SpringSecurity框架
简介
-
是一个功能强大且高度可定制的身份验证和访问控制框架。
-
是一个专注于为Java应用程序提供身份验证和授权的框架。
-
是一个能够为基于Spring的企业应用系统提供声明式(注解)的安全访问控制解决方案的安全框架。
-
它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
-
一句话来概括,SpringSecurity 是一个安全框架。
-
官网: https://spring.io/projects/spring-security
中文文档: https://www.springcloud.cc/spring-security.html
-
声明式权限鉴定框架
-
了解一下:还有一个声明式权限鉴定框架:shiro
-
使用的时候就是:过滤器+注解
特征
- 对身份验证和授权的全面且可扩展的支持
- 防止会话固定、点击劫持、跨站点请求伪造等攻击
- Servlet API 集成
- 可选与 Spring Web MVC 集成
授权的数据模型(RBAC)
-
RBAC(Role-Based Access Control):基于角色的访问控制
if(主体.hasRole("总经理角色id")){ 查询工资 }
-
RBAC(Resource-Based Access Control):基于资源(或权限)的访问控制
if(主体.hasPermission("查询工资") ){ 查询工资 }
-
前者可扩展性差,后者可扩展性强
使用
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
使用配置文件
#如果使用了配置类,这里就失效了
spring:
security:
user:
name: admin
password: 123
使用配置类
/**
* SpringSecurity的配置类
* 这里配置了,配置文件里的就失效了
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置Security登录的用户信息
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中配置用户
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder().encode("123"))
//配置角色
//如果为用户配置的是角色,那么系统会在角色字符串前追加一个ROLE_的前缀,表示当前是角色
.roles("ADMIN")
.and()
.withUser("tom")
.password(passwordEncoder().encode("123"))
.roles("MANAGER")
.and()
.withUser("jerry")
.password(passwordEncoder().encode("123"))
.roles("STUDENT");
}
/**
* 从SpringSecurity5.0开始,强制要求密码加密,在测试类中有测试代码
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
密码加密
@Test
void testPassword(){
String source = "123";
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String p1 = passwordEncoder.encode(source);
System.out.println(p1);
String p2 = passwordEncoder.encode(source);
System.out.println(p2);
String p3 = passwordEncoder.encode(source);
System.out.println(p3);
//密码加密对比
System.out.println(passwordEncoder.matches(source, p1));
System.out.println(passwordEncoder.matches(source, p2));
System.out.println(passwordEncoder.matches(source, p3));
}
路径与权限
@RestController
public class CommonController {
@GetMapping("/admin/query")
public String adminQuery(){
return "admin-query";
}
@GetMapping("/manager/save")
public String managerSave(){
return "manager-save";
}
@GetMapping("/student/remove")
public String studentRemove(){
return "student-remove";
}
}
/**
* 配置路径与权限的关系
* 没有配置的路径,可以正常访问
* 权限不足响应状态码是 403
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//此方法中http参数必须调用formLogin()否则原生登录和登录页面都会消失
http.formLogin();
http.authorizeHttpRequests()
//编程式权限鉴定(推荐使用声明式 既注解)
.antMatchers("/admin/query").hasRole("ADMIN")
.antMatchers("/manager/**").hasRole("MANAGER")
.antMatchers("/student/remove").hasRole("STUDENT")
//所有的请求都必须进行身份认证
.anyRequest().authenticated();
}
权限不足页面:resources/static/error/403.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>权限鉴定失败</title>
</head>
<body>
<div style="text-align: center">
<h1>403你没有权限访问该资源!</h1>
</div>
</body>
</html>
获取登录信息
/**
* 获取用户登录信息的两种方法
* 获取到的密码是空的
*/
@RestController
public class GetUserInfoController {
/**
* 方法一:只能用在Controller层,Principal参数可以直接用
* 当前登录用户信息Principal
*/
@GetMapping("/userInfo")
public Principal userInfo(Principal principal){
return principal;
}
/**
* 方法二:也可用于Service层
* SecurityContext 安全框架的上下文对象
*/
@GetMapping("/getUserInfo")
public Object getUserInfo(){
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
return authentication;
}
}
配置方式
编程式配置
-
WebSecurityConfig.java 配置类
/** * SpringSecurity的配置类 * 这里配置了,配置文件里的就失效了 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 配置Security登录的用户信息 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //在内存中配置用户 auth.inMemoryAuthentication() .withUser("admin") .password(passwordEncoder().encode("123")) //配置 角色 //如果为用户配置的是角色,那么系统会在角色字符串前追加一个ROLE_的前缀,表示当前是角色 .roles("ADMIN") //配置 权限 //参数为:权限字符串 格式为-->模块名称:操作名称 .authorities("sys:query", "sys:save", "sys:update", "sys:delete") .and() .withUser("tom") .password(passwordEncoder().encode("123")) .roles("MANAGER") .authorities("sys:query", "sys:save") .and() .withUser("jerry") .password(passwordEncoder().encode("123")) .roles("STUDENT"); } /** * 配置路径与权限的关系 * 没有配置的路径,可以正常访问 * 权限不足响应状态码是 403 */ @Override protected void configure(HttpSecurity http) throws Exception { //此方法中http参数必须调用formLogin()否则原生登录和登录页面都会消失 http.formLogin(); http.authorizeHttpRequests() //编程式权限鉴定(推荐使用声明式 既注解) .antMatchers("/admin/query").hasRole("ADMIN") .antMatchers("/manager/**").hasRole("MANAGER") .antMatchers("/student/remove").hasRole("STUDENT") .antMatchers("/sys/query").hasAuthority("sys:query") .antMatchers("/sys/save").hasAuthority("sys:save") .antMatchers("/sys/update").hasAuthority("sys:update") .antMatchers("/sys/delete").hasAuthority("sys:delete") //所有的请求都必须进行身份认证 .anyRequest().authenticated(); } /** * 从SpringSecurity5.0开始,强制要求密码加密,在测试类中有测试代码 */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
-
Controller类
@RestController @RequestMapping("/sys") public class SysController { @GetMapping("/query") public String query(){ return "sys-query"; } @GetMapping("/save") public String save(){ return "sys-save"; } @GetMapping("/update") public String update(){ return "sys-update"; } @GetMapping("/delete") public String delete(){ return "sys-delete"; } }
注解式配置
-
在配置类上开启注解支持
@Configuration //开启注解式鉴权,prePostEnabled开启方法的前置与后置鉴权 @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }
-
在方法上开启鉴权
@RestController @RequestMapping("/sys") public class SysController { @GetMapping("/query") @PreAuthorize("hasAuthority('sys:query')") public String query(){ return "sys-query"; } @GetMapping("/save") @PreAuthorize("hasAuthority('sys:save')") public String save(){ return "sys-save"; } @GetMapping("/update") @PreAuthorize("hasAuthority('sys:update')") public String update(){ return "sys-update"; } @GetMapping("/delete") @PreAuthorize("hasAuthority('sys:delete')") public String delete(){ return "sys-delete"; } }
处理器
- 用于异步请求
鉴权处理器
-
权限鉴定处理器实现类:AccessDeniedHandlerImpl.java
/** * 权限不足(鉴权失败)处理器 */ @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { /** * 鉴权失败处理方法 * @param request 原生请求对象 * @param response 原生响应对象 * @param accessDeniedException 鉴权失败异常对象 */ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { //设置内容类型 // response.setContentType("application/json;charset=utf-8"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); //可以使用spring提供的常量 //将json数据写出到客户端 //ObjectMapper是将Java对象转换为json字符串,将json字符串转换为Java对象的工具类 ObjectMapper objectMapper = new ObjectMapper(); response.getWriter().write(objectMapper.writeValueAsString(Result.error(-20, "您没有权限访问"))); } }
-
配置类 自动装配
//自动装配处理器 //鉴权失败处理器 @Autowired private AccessDeniedHandler accessDeniedHandler;
-
配置鉴权失败处理器
@Override protected void configure(HttpSecurity http) throws Exception { ... //配置鉴权失败处理器 http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); ... }
登录成功/失败处理器
-
主要用于异步请求,前后端分离
-
返回的是 json 字符串
-
登录成功处理器实现类
/** * 身份认证成功处理器 */ @Component public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { /** * 身份认证成功之后的处理 * @param request 请求对象 * @param response 响应对象 * @param authentication 当前登录用户对象 */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //设置内容类型 response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); //将json数据写出到客户端 response.getWriter().write(new ObjectMapper().writeValueAsString(Result.success("登录成功"))); } }
-
登录失败处理器实现类
/** * 身份认证失败处理器 */ @Component public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler { /** * 身份认证失败之后处理 * @param request 请求对象 * @param response 响应对象 * @param exception 身份认证失败异常对象 */ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //设置内容类型 response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); String message = ""; if(exception instanceof UsernameNotFoundException){ message = "用户名有误"; }else if(exception instanceof BadCredentialsException){ message = "用户名或密码有误"; }else if(exception instanceof LockedException){ message = "当前用户被锁定,请联系管理员"; }else if(exception instanceof DisabledException){ message = "当前用户被禁用,请联系管理员"; }else if(exception instanceof AccountExpiredException){ message = "账号已过期,请联系管理员"; }else if(exception instanceof CredentialsExpiredException){ message = "密码已过期,请联系管理员"; } response.getWriter().write(new ObjectMapper().writeValueAsString(Result.error(-10, message))); } }
-
自动注入到配置类
//身份认证成功与失败处理器 @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler;
-
配置处理器
@Override protected void configure(HttpSecurity http) throws Exception { //此方法中http参数必须调用formLogin()否则原生登录和登录页面都会消失 http.formLogin() //同步请求处理 // .successForwardUrl() // .failureUrl(); //异步请求处理 .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler); ... }
源码分析
-
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。
-
根据前边知识的学习,可以通过Filter或AOP等技术来实现,SpringSecurity对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
-
当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YHlHnBb8-1661775322058)(E:\JAVA\DLJD\我的笔记\myImages\SpringSecurity知识点总结\clip_image002.png)]
-
FilterChainProxy 是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理
过滤器
- 过滤器链
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oPt2bX3R-1661775322060)(E:\JAVA\DLJD\我的笔记\myImages\SpringSecurity知识点总结\clip_image002-1661216957977.png)] - SecurityContextPersistenceFilter
- 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
- UsernamePasswordAuthenticationFilter
- 身份认证过滤器
- 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,这些都可以根据需求做相关改变;
- FilterSecurityInterceptor
- 权限鉴定过滤器
- 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问;
- ExceptionTranslationFilter
- 异常捕获过滤器
- 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
自定义鉴权
-
前面我们在配置类中设置用户名和密码,其实存储在了内存中
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //在内存中配置用户 auth.inMemoryAuthentication() .withUser("admin") .password(passwordEncoder().encode("123")) .roles("ADMIN"); }
-
而实际开发中我们想要使用数据库就要实现UserDetailsService接口,
-
角色和权限可以同时配置
-
service–> UserDetailsServiceImpl
/** * 实现UserDetailsService方法,可以查自己的数据库,不用默认的了 * 在SpringSecurity中整个身份认证操作都由它自己独立完成 * 程序员仅需要提供用户的查询方式即可 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("用户名:" + username); //模拟从数据库查询用户: 用户信息暂时使用静态数据替代 return User.withUsername(username) .password(new BCryptPasswordEncoder().encode("123")) .authorities("sys:query", "sys:delete", "ROLE_ADMIN") .build(); } }
-
WebSecurityConfig 配置类
/** * 注入自定义用户查询类UserDetailsService的实现类 */ @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //配置自定义的UserDetailsService实现类来获取用户信息 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
-
Controller
@RestController public class CommonController { @GetMapping("/admin/query") @PreAuthorize("hasRole('ADMIN')") public String adminQuery(){ return "admin-query"; } @GetMapping("/manager/save") @PreAuthorize("hasRole('MANAGER')") public String managerSave(){ return "manager-save"; } @GetMapping("/student/remove") @PreAuthorize("hasRole('STUDENT')") public String studentRemove(){ return "student-remove"; } }
前后端分离项目-pms
自定义用户对象
-
实现了UserDetails接口,后面的service层需要使用
-
包括用户的属性、用户的所有权限、用户的所有角色
public class LoginUser implements UserDetails { private Integer id; private String username; private String mobile; private String nickname; private String email; private String gender; private String avatar; private String userpwd; private String userType; private String status; /** * 用户拥有的角色名称列表 */ private List<String> roleNameList; /** * 用户拥有的权限字符串列表 */ private List<String> percodeList; /** * 当前登录用户拥有的权限 */ @Override //json序列化忽略属性,转json是将会被忽略 @JsonIgnore public Collection<? extends GrantedAuthority> getAuthorities() { HashSet<GrantedAuthority> grantedAuthorityHashSet = new HashSet<>(); //添加角色 if(!CollectionUtils.isEmpty(roleNameList)){ for (String roleName : roleNameList) { if(StringUtils.hasLength(roleName)){ SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleName); grantedAuthorityHashSet.add(simpleGrantedAuthority); } } } //添加权限 if(!CollectionUtils.isEmpty(percodeList)){ for (String percode : percodeList) { if(StringUtils.hasLength(percode)){ SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(percode); grantedAuthorityHashSet.add(simpleGrantedAuthority); } } } return grantedAuthorityHashSet; } @Override @JsonIgnore public String getPassword() { return this.userpwd; } @Override public String getUsername() { return this.username; } @Override @JsonIgnore public boolean isAccountNonExpired() { return true; } @Override @JsonIgnore public boolean isAccountNonLocked() { return true; } @Override @JsonIgnore public boolean isCredentialsNonExpired() { return true; } @Override @JsonIgnore public boolean isEnabled() { return "1".equals(this.status); } //get... set... }
获取用户信息
-
返回的是自定义的用户对象
-
根据用户名获取用户的信息、角色和权限,统统获取
-
Service层 —> UserDetailsServiceImpl
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserService sysUserService; @Autowired private SysRoleService sysRoleService; @Autowired private SysPermissionService sysPermissionService; /** * 根据用户名查询用户 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.getOne(new QueryWrapper<SysUser>().eq("username", username)); if(sysUser == null){ throw new UsernameNotFoundException("用户不存在"); } //根据用户ID查询当前用户对应的角色和权限 List<SysRole> roleList = sysRoleService.listByUserId(sysUser.getId()); List<SysPermission> permissionList = sysPermissionService.listByUserId(sysUser.getId()); //创建用户登录对象 LoginUser loginUser = new LoginUser(); //复制对象属性值 BeanUtils.copyProperties(sysUser, loginUser); //设置角色名称和权限字符串列表 loginUser.setRoleNameList(roleList.stream().map(SysRole::getRolename).collect(Collectors.toList())); loginUser.setPercodeList(permissionList.stream().map(SysPermission::getPercode).collect(Collectors.toList())); //loginUser实现了UserDetails接口,可以直接返回 return loginUser; } }
处理器
-
前面的鉴权处理器、登录成功、失败处理器,都可以拿来直接用
-
未登录处理器
/** * 未认证处理器(未登录) */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { //设置内容类型 // response.setContentType("application/json;charset=utf-8"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); //将json数据写出到客户端 // response.getWriter().write(new ObjectMapper().writeValueAsString(Result.error(-10, "未登陆,请先登陆"))); response.getWriter().write(JsonUtil.toString(Result.error(-10, "未登陆,请先登陆"))); } }
-
登出成功处理器
/** * 登出成功处理器 */ @Component public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //设置内容类型 response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); //将json数据写出到客户端 // response.getWriter().write(new ObjectMapper().writeValueAsString(Result.success())); response.getWriter().write(JsonUtil.toString(Result.success())); } }
配置类
-
配置用户信息、权限、处理器
/** * SpringSecurity配置类 * 开启注解式鉴权 */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 依赖注入 5个处理器 userDetailsService */ @Autowired private AccessDeniedHandler accessDeniedHandler; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Autowired private UserDetailsService userDetailsService; /** * 配置用户信息 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } /** * 配置http安全校验等 */ @Override protected void configure(HttpSecurity http) throws Exception { http //开启跨域 .cors() .and() //关闭跨站请求伪造防护,如果不是前后端分离可以不关闭 .csrf().disable() //请求权限的配置 .authorizeHttpRequests() //配置url白名单(无需进行身份认证的url) //permitAll()拥有全部权限 anonymous()匿名访问(不登录时可访问,登陆后不可访问) .antMatchers("/auth/login", "/webjars/**", "/swagger-resources/**", "/doc.html", "/v2/api-docs").permitAll() //所有请求都需要进行身份认证 .anyRequest().authenticated() .and() //配置无权限处理器,未认证处理器 .exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint) .and() //配置登录相关信息 // .formLogin().loginProcessingUrl("/auth/login").usernameParameter("username").passwordParameter("userpwd") // .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler) // .and() //配置登出 .logout().logoutUrl("/auth/logout").logoutSuccessHandler(logoutSuccessHandler) .and() //添加自定义过滤器 .addFilter(loginAuthenticationFilter()) .addFilter(new JwtTokenVerifyFilter(authenticationManagerBean())) //前后台分离的项目中,不再使用HttpSession维持会话,直接禁用 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } /** * 配置自定义的登录过滤器 */ @Bean public LoginAuthenticationFilter loginAuthenticationFilter() throws Exception { LoginAuthenticationFilter loginAuthenticationFilter = new LoginAuthenticationFilter(); loginAuthenticationFilter.setFilterProcessesUrl("/auth/login"); loginAuthenticationFilter.setUsernameParameter("username"); loginAuthenticationFilter.setPasswordParameter("userpwd"); loginAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); loginAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); //设置身份认证管理对象 loginAuthenticationFilter.setAuthenticationManager(authenticationManagerBean()); return loginAuthenticationFilter; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
Controller层-定义权限
- 在Controller层添加 @PreAuthorize(“hasAuthority(‘sys:user:query’)”) 注解,设置访问路径的权限
@GetMapping("/list") @PreAuthorize("hasAuthority('sys:user:query')") public Result list(){ return Result.success(sysUserService.list()); }
自定义过滤器
-
默认的过滤器接收的是名值对,无法接受json格式,为了解决这个问题,所以自定义一个过滤器
/** * 自定义登录过滤器 * 要求:前端提交post请求,并且数据为json字符串 */ public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //判断请求是否为post请求 if(!"POST".equalsIgnoreCase(request.getMethod())){ throw new AuthenticationServiceException("登录方法不支持" + request.getMethod() + "请求方式"); } //判断数据是否为json if(!request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){ throw new AuthenticationServiceException("登录数据必须为json格式"); } //接收json数据 SysUser sysUser = null; try { sysUser = JsonUtil.toBean(request.getInputStream(), SysUser.class); } catch (IOException e) { e.printStackTrace(); } UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getUserpwd()); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
-
用到的 Json 工具类
/** * jackson工具类 */ public class JsonUtil { public static final ObjectMapper mapper = new ObjectMapper(); /** * 将对象转换为json字符串 */ public static String toString(Object obj){ if(obj == null){ return null; } if(obj.getClass() == String.class){ return (String) obj; } try { return mapper.writeValueAsString(obj); } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } /** * 将字符串转换为对象 */ public static <T> T toBean(String json, Class<T> tClass){ try { return mapper.readValue(json, tClass); } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } /** * 将字节流(传输json串)转换为对象 */ public static <T> T toBean(InputStream in, Class<T> tClass){ try { return mapper.readValue(in, tClass); } catch (IOException e) { e.printStackTrace(); } return null; } }
-
在配置类中配置登录过滤器,前面写过了,这里就不重复了
获取当前用户
-
工具类
public class SecurityUtil { /** * 获取当前登录用于的Authentication对象 */ public static Authentication getAuthentication(){ return SecurityContextHolder.getContext().getAuthentication(); } /** * 获取自定义的登录对象LoginUser */ public static LoginUser getLoginUser(){ return (LoginUser) getAuthentication().getPrincipal(); } /** * 获取用户账号 */ public static String getUsername(){ return getLoginUser().getUsername(); } /** * 获取用户ID */ public static Integer getUserId(){ return getLoginUser().getId(); } }
-
使用 Controller中能用到
/** * 新增数据 */ @PostMapping("/save") @PreAuthorize("hasAuthority('sys:user:save')") public Result save(@RequestBody SysUser entity){ entity.setCreateUser(SecurityUtil.getUsername()); entity.setUpdateUser(SecurityUtil.getUsername()); entity.setCreateTime(new Date()); entity.setUpdateTime(new Date()); return Result.success(sysUserService.add(entity)); } /** * 编辑数据 */ @PutMapping("/edit") @PreAuthorize("hasAuthority('sys:user:update')") public Result edit(@RequestBody SysUser entity){ entity.setUpdateUser(SecurityUtil.getUsername()); entity.setUpdateTime(new Date()); return Result.success(sysUserService.edit(entity)); }
JWT
认识JWT
- Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准
- 该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
- 官网:https://jwt.io/
- 可以代替session使用
认证方式
Session认证
- 用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中
- 客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验
- 当用户退出系统或session过期销毁时,客户端的session_id也就无效了
Token认证
- 用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,
- 每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
- Redis 存的用户信息 共享session (分布式中)
- 基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;
- 基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。
- 如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
使用
-
使用思路
- 前端通过用户名和密码换取token
- 前端发送请求需要携带UUID/Token,后端会校验
-
依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.2</version> </dependency>
工具类
-
JwtUtil
/** * jwt的工具类 */ public class JwtUtil { /** * 密钥字符串 */ private static final String SECRET_KEY = "powernode2022powernode2022powernode2022"; /** * 生成jwt字符串 */ public static String generateToken(LoginUser loginUser, long ttl){ long currentTimeMillis = System.currentTimeMillis(); return Jwts.builder() //在荷载部分中添加自定义的数据 .claim("loginUser", JsonUtil.toString(loginUser)) // .addClaims() //唯一标识 .setId(UUID.randomUUID().toString()) //签发时间:字符串生成时间 .setIssuedAt(new Date(currentTimeMillis)) //签发人 .setSubject("POWERNODE") //过期时间 .setExpiration(new Date(currentTimeMillis+ttl)) //生成jwt时使用的算法和密钥 .signWith(generalKey(), SignatureAlgorithm.HS256) .compact(); } /** * 加密密钥 */ public static SecretKey generalKey(){ return new SecretKeySpec(SECRET_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName()); } /** * 解析jwt */ public static Claims parseToken(String jwtToken){ Claims claims = Jwts.parserBuilder() //设置签名的密钥 .setSigningKey(generalKey()) .build() //设置需要解析的jwt字符串 .parseClaimsJws(jwtToken) .getBody(); return claims; } /** * 获取用户登录对象 */ public static LoginUser getLoginUser(String jwtToken){ Claims claims = parseToken(jwtToken); String loginUserJosn = claims.get("loginUser").toString(); return JsonUtil.toBean(loginUserJosn, LoginUser.class); } public static void main(String[] args) { LoginUser loginUser = new LoginUser(); loginUser.setUsername("jerry"); loginUser.setPercodeList(Arrays.asList("sys:query", "sys:save", "sys:update")); String token = generateToken(loginUser, 1000*60*60*24); System.out.println(token); } }
登录时向前端响应JwtToken
-
处理器
/** * 身份认证成功处理器 */ @Component public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { /** * 身份认证成功之后的处理 * @param request 请求对象 * @param response 响应对象 * @param authentication 当前登录用户对象 */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //设置内容类型 response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); //获取当前用户登录 LoginUser loginUser = SecurityUtil.getLoginUser(); String jwtToken = JwtUtil.generateToken(loginUser, 1000*60*60*24); //将jwt字符串设置到响应头中 response.setHeader("Authorization", "Bearer " + jwtToken); //将json数据写出到客户端 // response.getWriter().write(new ObjectMapper().writeValueAsString(Result.success())); response.getWriter().write(JsonUtil.toString(Result.success(loginUser))); } }
-
跨域配置中设置响应头
//设置暴露给前端响应头 corsConfiguration.addExposedHeader("Authorization");
登录认证-过滤器
-
过滤器
public class JwtTokenVerifyFilter extends BasicAuthenticationFilter { private final AuthenticationManager authenticationManager; /** * 有参数的构造方法 */ public JwtTokenVerifyFilter(AuthenticationManager authenticationManager) { super(authenticationManager); this.authenticationManager = authenticationManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //从请求头中获取jwtToken字符串 String token = request.getHeader("Authorization"); if(!StringUtils.hasLength(token) || !token.startsWith("Bearer ")){ chain.doFilter(request, response); //设置未登录提示 response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JsonUtil.toString(Result.error(-10, "未登陆,请先登陆"))); }else{ //从jwt字符串中获取LoginUser对象 LoginUser loginUser = JwtUtil.getLoginUser(token.replace("Bearer ", "")); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); //将UsernamePasswordAuthenticationToken放入SecurityContext上下文 SecurityContextHolder.getContext().setAuthentication(authRequest); chain.doFilter(request, response); } } }
-
配置过滤器,禁用session
//添加自定义过滤器 .addFilter(loginAuthenticationFilter()) .addFilter(new JwtTokenVerifyFilter(authenticationManagerBean())) //前后台分离的项目中,不再使用HttpSession维持会话,直接禁用 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
esponse, FilterChain chain) throws IOException, ServletException {
//从请求头中获取jwtToken字符串
String token = request.getHeader(“Authorization”);
if(!StringUtils.hasLength(token) || !token.startsWith("Bearer ")){
chain.doFilter(request, response);
//设置未登录提示
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JsonUtil.toString(Result.error(-10, “未登陆,请先登陆”)));
}else{
//从jwt字符串中获取LoginUser对象
LoginUser loginUser = JwtUtil.getLoginUser(token.replace("Bearer ", “”));
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//将UsernamePasswordAuthenticationToken放入SecurityContext上下文
SecurityContextHolder.getContext().setAuthentication(authRequest);
chain.doFilter(request, response);
}
}
}
- 配置过滤器,禁用session
```java
//添加自定义过滤器
.addFilter(loginAuthenticationFilter())
.addFilter(new JwtTokenVerifyFilter(authenticationManagerBean()))
//前后台分离的项目中,不再使用HttpSession维持会话,直接禁用
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);