在 Spring Security
中,默认的登陆方式是以表单形式进行提交参数的。可以参考前面的几篇文章,但是在前后端分离的项目,前后端都是以 JSON 形式交互的。一般不会使用表单形式提交参数。所以,在 Spring Security
中如果要使用 JSON 格式登录,需要自己来实现。那本文介绍两种方式使用 JSON 登录。
- 方式一:重写
UsernamePasswordAuthenticationFilter
过滤器 - 方式二:自定义登录接口
1. 通过源码分析可以知道(打断点,往前捋),登录参数的提取在 UsernamePasswordAuthenticationFilter
过滤器中提取的,因此我们只需要模仿UsernamePasswordAuthenticationFilter
过滤器重写一个过滤器,替代原有的UsernamePasswordAuthenticationFilter
过滤器即可。
UsernamePasswordAuthenticationFilter
的源代码如下:
重写其中的attemptAuthentication方法,默认表单认证,需要修改为兼容json认证 (注意:其中需要定义有参构造方法,且传入authenticationManager类):
- 1、当前登录请求是否是 POST 请求,如果不是,则抛出异常。
- 2、判断请求格式是否是 JSON,如果是则走我们自定义的逻辑,如果不是则调用
super.attemptAuthentication
方法,进入父类原本的处理逻辑中;当然也可以抛出异常。- 3、如果是 JSON 请求格式的数据,通过 ObjectMapper 读取 request 中的 I/O 流,将 JSON 映射到Map 上。
- 4、从 Map 中取出 code key的值,判断验证码是否正确,如果验证码有错,则直接抛出异常。如果对验证码相关逻辑感到疑惑,请前往:Spring Security 在登录时如何添加图形验证码验证
- 5、根据用户名、密码构建
UsernamePasswordAuthenticationToken
对象,然后调用官方的方法进行验证,验证用户名、密码是否真实有效。
package com.cmit.abc.backend.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
/**
* 这里只是将用户名/密码的获取方案重新修正下,改为了从 JSON 中获取用户名密码
*/
@Slf4j
// @Component // SecurityConfig中@Bean了
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JsonUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager){
super(authenticationManager);
}
/* 构造方法和重写set方法,两种方式底层原理都一样。可以点进去看父类的源码*/
// @Autowired
// @Override
// public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// super.setAuthenticationManager(authenticationManager);
// }
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 需要是 POST 请求
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// json类型请求处理方式
// System.out.println(">>>>>>>>>>>>>>>>>>>>>>>" + request.getContentType());
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
// Map<String, String> loginData = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()) {
// 通过ObjectMapper读取request中的I/O流,将JSON映射到Map上
Map<String, String> authenticationBean = mapper.readValue(is, Map.class);
authRequest = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"), authenticationBean.get("password"));
} catch (IOException e) {
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken("", "");
} finally {
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
} else {
// 其他类型的请求依旧走原来的处理方式
return super.attemptAuthentication(request, response);
}
}
}
2. 配置身份认证管理器SecurityConfig(添加自定义json认证过滤器):
http.addFilterBefore(jsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
package com.cmit.abc.backend.config;
import com.cmit.abc.backend.pojo.entity.Permission;
import com.cmit.abc.backend.service.PermissionService;
import com.cmit.abc.backend.service.SecurityAuthenticationHandler;
import com.cmit.abc.backend.service.SecurityUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
/**
* Security配置类
* 采用适配器模式,继承后SecurityConfig可以看做是WebSecurityConfigurer
*/
@Configuration
// @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SecurityUserDetailsService securityUserDetailsService;
@Autowired
SecurityAuthenticationHandler securityAuthenticationHandler;
@Autowired
PermissionService permissionService;
// @Autowired
// // @Lazy
// JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// @Autowired
// // @Lazy
// JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter;
/**
* 身份安全管理器
*
* 使用自定义用户认证
*
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(securityUserDetailsService).passwordEncoder(getBCryptPasswordEncoder()); // 默认使用BCrypt,可以不用指定,但是必须@Bean上
auth.userDetailsService(securityUserDetailsService); // 使用自定义用户认证
}
/**
* Web-Security
* WebSecurity是包含HttpSecurity的更大的一个概念
*
* 放行请求
*
*/
@Override
public void configure(WebSecurity web) {
// 解决静态资源被拦截的问题(注意不能加prefix,即这里在配置文件配置的全链路前缀:/abc(server.servlet.context-path: /abc))
web.ignoring().antMatchers("/css/**", "/images/**", "/js/**")
// 放行微信小程序的相关请求
.antMatchers("/health/check",
"/partner/record/save", "/partner/record/info", "/partner/record/type/list", "/partner/record/score/update",
"/account/wx/**",
"/company/list");
}
/**
* Http-Security
* HttpSecurity 是WebSecurity 的一部分
*
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 获取所有权限数据,需要把“权限表的全部数据”都添加到spring security的缓存中(url <-> tag 二者一一对应匹配放入缓存)
List<Permission> permissionList = permissionService.findAll();
// http.authorizeRequests().antMatchers("/health/test").hasAnyAuthority("ADMIN");
for (Permission p : permissionList) {
http.authorizeRequests().antMatchers(p.getUrl()).hasAuthority(p.getTag());
}
/*
① http.httpBasic() // 开启httpBasic认证
.and().authorizeHttpRequests().anyRequest().authenticated(); // 配置request请求 -> 任何request请求 -> 需要认证
② http.formLogin() // 开启表单认证
// .loginPage("/login.html") // 默认,可以不用填写
.and().authorizeRequests().anyRequest().authenticated(); // 配置request请求 -> 任何request请求 -> 需要认证
*/
/**
* formLogin是表单登录基于session-cookie机制进行用户认证的,
* 而前后端分离一般使用jwt 即用户状态保存在客户端中,前后端交互通过api接口 无session生成,
* 所以我们不需要配置formLogin
*
* 注意:不开启表单认证,则不会进入loadUserByUsername方法!!
*/
http
.formLogin() // 开启表单认证
// .loginPage("/login.html")
.loginProcessingUrl("/user/login")// 默认"/login",可以不用填写
.successHandler(securityAuthenticationHandler)
.failureHandler(securityAuthenticationHandler)
.and()
.logout()
.logoutUrl("/user/logout")// 默认"/logout",可以不用填写
.logoutSuccessHandler(securityAuthenticationHandler)
// 配置拦截规则
.and()
.authorizeRequests()
// 配置/login 为匿名访问 防止被拦截无法进行登录
//.antMatchers("/account/login").anonymous()// 允许匿名访问
// 统一挪到configure(WebSecurity web)中了
// .antMatchers("/health/check", "/partner/record/info", "/partner/record/score/update").permitAll() //放行请求(注意不能加prefix,即这里在配置文件配置的全链路前缀:/abc(server.servlet.context-path: /abc))
.anyRequest().authenticated() // 配置request请求 -> 任何request请求 -> 需要认证
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(securityAuthenticationHandler)
.accessDeniedHandler(securityAuthenticationHandler)
// 自定义过滤器
.and()
.addFilter(jwtAuthenticationTokenFilter())
// .addFilter(jwtAuthenticationTokenFilter);
// 验证码过滤器放在UsernamePassword过滤器之前
// .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
// 添加自定义json认证过滤器
.addFilterBefore(jsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 关闭csrf(跨站请求伪造)防护
// 前后端分离项目需要关闭csrf。否则前端和后端的两个域名一般是不一样的,会被拦截。
http.csrf().disable();
// 允许cors(跨域)// 前后端分离必配(大部分是因为端口号或域名不同)
http.cors().configurationSource(corsConfigurationSource());
// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 跨域信息配置源
*
*/
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration(); // 允许跨域的站点
// corsConfiguration.addAllowedOrigin("*"); // 允许跨域的http方法
corsConfiguration.addAllowedOriginPattern("*"); // 允许跨域的http方法 // 因为springboot升级成2.4.0以上时对AllowedOrigin设置发生了改变,不能有”*“
corsConfiguration.addAllowedMethod("*"); // 允许跨域的请求头
corsConfiguration.addAllowedHeader("*"); // 允许携带凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); // 对所有url都生效
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return urlBasedCorsConfigurationSource;
}
/**
* 默认使用BCrypt,可以不用指定,但是必须@Bean上
*/
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
// 因为security提供的该类并没有考虑前端加密的问题。我们需要重写其matches方法,该方法用于判断从前端接收的密码与数据库中的密码是否一致
// 我们设前端使用rsa对密码进行加密,后端使用BCrypt对密码进行加密
// return new SecurityPasswordEncoder();
}
/**
* security登录关键类是 AuthenticationManager认证管理器,
* 如果我们什么都不配置默认使用的是ProviderManager是ioc运行时注册的bean,我们无法在编译时声明注入到controller,
* 所以需要我们把默认的ProviderManager在编译期声明为bean
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/** 把上面的super.authenticationManager()传入封装到自定义过滤器中了 */
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() throws Exception {
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter = new JwtAuthenticationTokenFilter(authenticationManager());
return jwtAuthenticationTokenFilter;
}
/**
* json 格式认证支持
*
* 当我们替换了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,
* 那些失效的属性,都可以在配置 JsonUsernamePasswordAuthenticationFilter 实例的时候配置;还需要记得配置AuthenticationManager,否则启动时会报错。
*
*/
/** 把上面的super.authenticationManager()传入封装到自定义过滤器中了 */
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter = new JsonUsernamePasswordAuthenticationFilter(authenticationManager());
// 配置采用json形式认证时的各种回调处理器
jsonUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(securityAuthenticationHandler);
jsonUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(securityAuthenticationHandler);
jsonUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/user/login");// logout的url就不用设置了,使用上面的表单方式中的配置
return jsonUsernamePasswordAuthenticationFilter;
}
}
3.自定义各种Handler处理器
package com.cmit.abc.backend.service;
import com.alibaba.fastjson.JSONObject;
import com.cmit.abc.backend.pojo.dto.ResponseDTO;
import com.cmit.abc.backend.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* 自定义各种Handler处理器
*
* 登录成功或失败处理器,退出登录处理器等
*/
@Slf4j
@Component
public class SecurityAuthenticationHandler
implements AuthenticationSuccessHandler, AuthenticationFailureHandler,
LogoutSuccessHandler,
AccessDeniedHandler,
AuthenticationEntryPoint {
// implements SavedRequestAwareAuthenticationSuccessHandler, SimpleUrlAuthenticationFailureHandler, SimpleUrlLogoutSuccessHandler {
// extends SavedRequestAwareAuthenticationSuccessHandler {
// // 这个工具类,可以方便的帮助我们跳转页面
// RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// System.out.println("登录成功后,继续处理.............");
// 重定向到index页面
// redirectStrategy.sendRedirect(request, response, "/");
String token = jwtUtils.generateToken(((User)authentication.getPrincipal()).getUsername());
this.responseWrapper(response, HttpStatus.OK.value(), "登录成功!", Map.of(jwtUtils.getHeader(), token));
}
/**
* 登录失败后的处理逻辑
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
// System.out.println("登录失败后继续处理...............");
// 重定向到login页面
// redirectStrategy.sendRedirect(request, response, "/toLoginPage");
// 登录失败
String message = "";
if (exception instanceof BadCredentialsException) {
message = "用户名或者密码输入错误,请重新输入!";
} else if(exception instanceof LockedException) {
message = "账户被锁定,请联系管理员!";
}
this.responseWrapper(response, HttpStatus.UNAUTHORIZED.value(), message, Map.of("exception", exception.getMessage()));
}
/**
* 登出处理器
*
* ① 在用户退出登录时,我们需将原来的JWT置为空返给前端
* - 这样前端会将空字符串覆盖之前的jwt,JWT是无状态化的,销毁JWT是做不到的,JWT生成之后,只有等JWT过期之后,才会失效。因此我们采取置空策略来清除浏览器中保存的JWT。
* ② 同时我们还要将我们之前置入SecurityContext中的用户信息进行清除
* - 这可以通过创建SecurityContextLogoutHandler对象,调用它的logout方法完成
*
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// System.out.println("退出之后继续处理。。。");
// redirectStrategy.sendRedirect(request, response, "/login");
if(authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
//置空token
this.responseWrapper(response, HttpStatus.OK.value(), "退出成功!", Map.of(jwtUtils.getHeader(), ""));
}
/**
* 无权限访问的处理
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
this.responseWrapper(response, HttpStatus.FORBIDDEN.value(), "您无访问权限!", Map.of("exception", accessDeniedException.getMessage()));
}
/**
* 当BasicAuthenticationFilter(即JwtAuthenticationTokenFilter)认证失败的时候(token校验抛异常,无权限,且不在白名单中),会进入AuthenticationEntryPoint
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.error(authException.getMessage());
// 登录认证失败
this.responseWrapper(response, HttpStatus.UNAUTHORIZED.value(), "您未登录,请先登录!", Map.of("exception", authException.getMessage()));
}
private void responseWrapper(HttpServletResponse response, int state, String message, Map<String, Object> properties) throws IOException {
// 设置MIME,务必在getWriter()方法被调用之前调用
// response.setContentType("application/json;charset=UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setStatus(state);
// // Map<Object, Object> result = new HashMap<>();
// JSONObject resultObj = new JSONObject();
// resultObj.put("state", state);
// resultObj.put("message", message);
// Optional.ofNullable(properties).ifPresent(p -> resultObj.putAll(p));
// response.getWriter().write(resultObj.toString());
ResponseDTO<Map<String, Object>> res = ResponseDTO.response(state, message, properties);
response.getWriter().write(JSONObject.toJSONString(res));
}
}
补充:
4. 自定义JwtFilter过滤器
package com.cmit.abc.backend.config;
import com.cmit.abc.backend.service.SecurityUserDetailsService;
import com.cmit.abc.backend.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证jwt,获取username,设置到上下文(SecurityContextHolder)
* 这样后续我们就能通过调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()等方法获取到当前登录的用户信息了。
*/
@Slf4j
// @Component // SecurityConfig中@Bean了
public class JwtAuthenticationTokenFilter extends BasicAuthenticationFilter {
@Autowired
SecurityUserDetailsService securityUserDetailsService;
@Autowired
private JwtUtils jwtUtils;
public JwtAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的token
String token = request.getHeader(jwtUtils.getHeader());
// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
if (Strings.isNotBlank(token)) {
// 验证token
Claims claims = jwtUtils.parseJwtToken(token);
if (claims==null) {
throw new JwtException("token验证不通过");
}
if (jwtUtils.isTokenExpired(claims.getExpiration())) {
throw new JwtException("token已过期");
}
// //实际项目中可以把登录成功的用户实体保存到redis通过token取到填充即可 (这里为了简便我直接硬编码了)
String username = claims.getSubject();
// 获取用户的权限等信息
UserDetails userDetails = securityUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities());
//将认证通过的信息填充到安全上下文中(用于一次请求的授权)
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
}
5. 自定义一个UserDetails类,“实现”自Spring Security提供的UserDetailsService接口
package com.cmit.abc.backend.service;
import com.cmit.abc.backend.pojo.entity.Permission;
import com.cmit.abc.backend.pojo.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 基于数据库完成认证
*
* 两种场景,会调用该方法:
* ① 访问/login(登录)
* ② 访问其他链接(携带token访问)
*
*/
@Service
public class SecurityUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Autowired
PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* ① 查找用户
*/
User user = userService.findByUserName(username);
if(user == null){
throw new UsernameNotFoundException("用户名没找到:"+username);
}
/**
* ② 权限的集合
* 正常这里应该传递权限集合。由于我们还没有权限,所以先声明一个“空的”权限集合供new User()的参数使用着(因为构造方法里面不能传入null,所以伪装一下哈哈)
*/
// Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
// ==================================模拟数据库中获取权限=========================================
// if (username.equalsIgnoreCase("admin")) {
// authorities.add(new SimpleGrantedAuthority("ADMIN"));
// }else {
// authorities.add(new SimpleGrantedAuthority("MEMBER"));
// }
// ==================================模拟数据库中获取权限=========================================
List<Permission> permissionList = permissionService.findByUserId(user.getId());
permissionList.stream().forEach(p -> authorities.add(new SimpleGrantedAuthority(p.getTag())));
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
username,
// "{bcrypt}" + user.getPassword(), // noop表示不使用密码加密,bcrypt表示使用bcrypt作为加密算法
// true, // 启用账号
// true, // 账号未过期
// true, // 用户凭证是否过期
// true, // 用户是否锁定
user.getPassword(),
authorities
);
return userDetails;
}
/**
*
* BCrypt 强哈希算法
* 单向加密算法,相对于MD5等加密方式更加安全(BCrypt加密算法:经过salt和cost的处理,加密时间(百ms级)远远超过md5(大概1ms左右))
*
*/
// 测试BCrypt加密方法
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");
String encode1 = bCryptPasswordEncoder.encode("123456");
// 对比发现,可以两次加密结果不一样
System.out.println(encode + " <<<>>> " + encode1);
boolean matchesBoolean = bCryptPasswordEncoder.matches("123456", encode);
System.out.println(matchesBoolean);
/*
bcrypt加密后的字符串形如:
$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。
*/
}
}
6. permissionRepository.findByUserId(userId)的sql语句(JPA编写)
@Query(value = "select * from permission p, role_permission rp, role r, user_role ur, user u " +
"WHERE p.id = rp.permission_id and rp.role_id = r.id and r.id = ur.role_id and ur.user_id = u.id and u.id = :userId", nativeQuery = true)
List<Permission> findByUserId(long userId);
package com.cmit.abc.backend.pojo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
import java.io.Serializable;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@DynamicUpdate
@DynamicInsert
@Entity
//@EqualsAndHashCode//不能用@EqualsAndHashCode和@ToString,否则死循环内存溢出
@Table(name = "permission")
public class Permission implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "tag")
private String tag;
@Column(name = "url")
private String url;
@Column(name = "status")
private Integer status;
}
参考链接:
四:Spring Security 登录使用 JSON 格式数据_爱是与世界平行的博客-CSDN博客
Spring Security 使用JSON格式参数登录的两种方式 - 掘金 (juejin.cn)
(943条消息) SpringSecurity报错authenticationManager must be specified_小楼夜听雨QAQ的博客-CSDN博客