Spring Security 5.7 的简单搭建流程
1 环境搭建
1.1 pom 文件中依赖的引入
该流程搭建的是一个前后端分离的项目,环境搭建使用 Spring boot 2.7.10
和 Spring Security 5.7
,数据库使用 MySQL8
、druid
、 MyBatis-Plus
具体pom文件的引入为:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.oizys</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2 基本的配置
在Spring Security 5.7 之后的版本中 WebSecurityConfigurerAdapter 已经被弃用 我们不需要再去继承它了。现在的做法是先写一个配置类,然后为他添加 @EnableWebSecurity
、@Configuration
两个注解,而具体的配置方法我们写在 securityFilterChain()
方法中:
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests()
// 所有的请求都需要认证
.anyRequest().authenticated()
.and().formLogin()
// 关闭 csrf 防御
.and().csrf().disable()
.build();
}
}
2 自定义登录的处理
2.1 修改登录的处理
2.1.1 重写登录的过滤器
Spring Security 为我们准备了一个用 bootstrap 写好的登录页面,并且当我们没有通过登录的验证时自动跳转到该页面。但是我们的前后端交互使用的是 json 来传递数据,我们不希望它给我们返回一个页面,因此我们需要重写它原本用来处理登录请求的过滤器 UsernamePasswordAuthenticationFilter
,来实现我们的要求。
我们写一个 LoginFilter
来继承 UsernamePasswordAuthenticationFilter
,在我们自己定义的过滤器中我们需要做几件事:
- 判断前端提交的请求是否是 POST 方式
- 获取前端传来的 josn 并解析得到用户名和密码
- 将用户名和密码交给 Spring Security
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
try {
Map<String, String> userinfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// 采用 getUsernameParameter() 方式,我们可以自定义 json 数据中用户名和密码的 key
String username = userinfo.get(getUsernameParameter());
String password = userinfo.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
}
}
return super.attemptAuthentication(request, response);
}
}
我们与原本过滤器的不同在于,我们用对 json 的操作替换了原本从表单获取参数
2.1.2 编写我们自己的 User 对象来进行认证
public class CustomUser extends org.springframework.security.core.userdetails.User {
private static final long serialVersionUID = 4547932757380981967L;
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象
*/
private User user;
private String token;
public CustomUser(User user, Collection<? extends GrantedAuthority> authorities) {
super(user.getUsername(), user.getPassword(), authorities);
this.user = user;
}
public User getUser() {
return user;
}
public void setUser(User user) {
user.setPassword(null);
this.user = user;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
2.1.3 重写实现登录验证的具体service
Spring Security 将对与登录认证的具体业务通过 loadUserByUsername
方法向我们提供自定义的实现
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
UserService userService;
@Override
@ApiOperation("用户登录")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userInfo = userService.getUserInfoByUsername(username);
if (userInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
if (userInfo.getStatus() == 0) {
throw new RuntimeException("用户已经被禁用");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : userInfo.getRoleList()) {
authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
}
return new CustomUser(userInfo, authorities);
}
}
2.2 对返回结果进行处理
与登录过滤器类似,在 Spring Security 为为我们返回数据的时候我们也希望它通过 json 的方式来对结果进行处理,因此我们需要重写一些类实现 json 的返回
2.2.1 对登录成功的处理
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", 200);
result.put("message", "登录成功");
response.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(json);
}
}
2.2.2 对登录成失败的处理
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>
();
result.put("code", 500);
result.put("message", exception.getMessage());
result.put("data", null);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
2.2.3 对注销成功的处理
public class MyLogoutSuccessHandler implements
LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", 200);
result.put("message", "注销成功");
result.put("data", null);
response.setContentType("application/json;charset=UTF-8");
String s = new
ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
2.3 将我们的自定义实现加入到配置类中
我们需要将我们自己写的 LoginFilter
进行配置然后交给 Spring Security
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Resource
UserDetailsServiceImpl userDetailsService;
/**
* 自定义登录过滤器的配置
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public LoginFilter loginFilter(AuthenticationManager authenticationManager) {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/login");
// 可以自定义用户名和密码的 key
// loginFilter.setUsernameParameter("uname");
// loginFilter.setPasswordParameter("passwd");
loginFilter.setAuthenticationManager(authenticationManager);
loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return loginFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests()
// 所有的请求都需要认证
.anyRequest().authenticated()
// 开启登录功能
.and()
// 指定我们的 loginFilter 添加到过滤器链的位置
.addFilterAt(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class)
// 指定我们自定义的 Service 实现类
.userDetailsService(userDetailsService)
.formLogin()
// 开启注销功能
.and()
.logout()
.logoutSuccessHandler(new MyLogoutSuccessHandler())
// 关闭 csrf 防御
.and().csrf().disable()
.build();
}
}
3 对密码加密的自定义
3.1 密码加密
实际密码比较是由 PasswordEncoder
完成的,因此只需要使用PasswordEncoder 不同实现就可以实现不同方式加密
- encode 用来进行明文加密的
- matches 用来比较密码的方法
- upgradeEncoding 用来给密码进行升级的方法
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
于是我们在新增和修改时只需要使用实现了 PasswordEncoder
接口的几个类就可以实现对密码的加密,也可以自己实现这个接口来自定义密码加密方式。
默认提供加密算法如下:
3.2 密码加密算法的升级更新
Spring Security 的密码由两个部分构成:加密算法的 id 和加密后的密码,id 表示该密码使用的加密方式。
当我们原本使用的密码加密方式已经过时,需要使用新的加密方式时,我们需要实现 PasswordEncoder
接口的 upgradeEncoding
方法,然后直接替换加密算法,Spring Security 会为我们根据加密算法的 id 自动更新密码的加密方式
@Service
public class UserDetailsServiceImpl implements UserDetailsService, UserDetailsPasswordService {
@Resource
UserService userService;
@Override
@ApiOperation("用户登录")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userInfo = userService.getUserInfoByUsername(username);
if (userInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
if (userInfo.getStatus() == 0) {
throw new RuntimeException("用户已经被禁用");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : userInfo.getRoleList()) {
authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
}
return new CustomUser(userInfo, authorities);
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
int result = userService.updatePasswordByUsername(user.getUsername(), newPassword);
CustomUser customUser = (CustomUser) user;
if (result >= 1) {
User u = customUser.getUser();
u.setPassword(newPassword);
customUser.setUser(u);
}
return customUser;
}
}
4 Remember-me 功能的实现
-
LoginFilter新增rememberme字段
String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER); if (!ObjectUtils.isEmpty(rememberValue)) { request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue); }
-
自定义RemembermeService实现类
public class PersistentTokenBasedRememberMeServicesImpl extends PersistentTokenBasedRememberMeServices { public PersistentTokenBasedRememberMeServicesImpl(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) { super(key, userDetailsService, tokenRepository); } @Override protected boolean rememberMeRequested(HttpServletRequest request, String parameter) { String paramValue = request.getAttribute(parameter).toString(); if (paramValue != null) { if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) { return true; } } return false; } }
-
配置SecurityConfig
@EnableWebSecurity @Configuration public class SecurityConfig { @Resource private DataSource dataSource; @Resource UserDetailsServiceImpl userDetailsService; /** * 自定义 remember-me 的实现 * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); // 只需要没有表时设置为 true jdbcTokenRepository.setCreateTableOnStartup(false); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } @Bean public RememberMeServices rememberMeServices() { return new PersistentTokenBasedRememberMeServicesImpl(UUID.randomUUID().toString(), userDetailsService, persistentTokenRepository()); } /** * 自定义登录过滤器的配置 * * @return */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } @Bean public LoginFilter loginFilter(AuthenticationManager authenticationManager) { LoginFilter loginFilter = new LoginFilter(); loginFilter.setFilterProcessesUrl("/login"); loginFilter.setAuthenticationManager(authenticationManager); loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler()); loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); loginFilter.setRememberMeServices(rememberMeServices()); return loginFilter; } /** * 鉴权、授权 * * @param http * @return * @throws Exception */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeRequests() // 特定权限访问页 .mvcMatchers("/user/**").hasRole("ADMIN") .anyRequest().authenticated() // 开启登录功能 .and() .addFilterAt(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class) .userDetailsService(userDetailsService) .formLogin() // 开启 remember me 功能 .and() .rememberMe() .rememberMeServices(rememberMeServices()) .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(10) // 开启注销功能 .and() .logout() .logoutSuccessHandler(new MyLogoutSuccessHandler()) // 关闭 csrf .csrf().disable() .and() .build(); } }
5 集成 Jwt 进行权限验证
5.1 引入依赖
在 Jwt 的官网 JSON Web Tokens - jwt.io 找到我们需要的依赖,在 pom 引用
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
5.2 编写相应的 Jwt 生成工具
public class JwtHelper {
public static String TOKEN_HEADER = "token";
//过期时间
private static int EXPIRATION_DAY = 7;
private static String ROLE = "role";
private static String SIGN = "#N!&SI#^";
public static String createToken(String username, List<Role> roles) {
ArrayList<String> rolesList = new ArrayList<>();
for (Role role : roles) {
rolesList.add(role.getRoleCode());
}
Calendar instance = Calendar.getInstance();
//过期时间设为7天
instance.add(Calendar.DATE, JwtHelper.EXPIRATION_DAY);
String token = null;
try {
token = JWT.create()
.withArrayClaim("role", rolesList.toArray(new String[0]))
.withClaim("username", username)
.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(JwtHelper.SIGN));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return token;
}
public static HashMap<String, Object> decode(String token) {
HashMap<String, Object> map = new HashMap<>();
DecodedJWT verify;
try {
verify = JWT.require(Algorithm.HMAC256(JwtHelper.SIGN)).build().verify(token);
} catch (Exception e) {
e.printStackTrace();
return map;
}
String username = verify.getClaim("username").asString();
String[] roles = verify.getClaim("role").asArray(String.class);
map.put("username", username);
map.put("roles", roles);
return map;
}
public static void setExpirationDay(int expirationDay) {
JwtHelper.EXPIRATION_DAY = expirationDay;
}
public static void setRole(String role) {
JwtHelper.ROLE = role;
}
public static void setSign(String sign) {
JwtHelper.SIGN = sign;
}
}
5.3 自定义 Jwt 配置
5.3.1 自定义 Jwt 过滤器
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//从请求头中获取token
String token = request.getHeader(JWTUtils.TOKEN_HEADER);
//没有直接跳过过滤器
if(ObjectUtils.isEmpty(token)){
chain.doFilter(request,response);
return;
}
//将token中的用户名和权限用户组放入Authentication对象,在之后实现鉴权
SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
super.doFilterInternal(request, response, chain);
}
//解析token获取用户信息
private UsernamePasswordAuthenticationToken getAuthentication(String token){
HashMap<String, Object> tokenInfo = JWTUtils.decode(token);
if(ObjectUtils.isEmpty(tokenInfo)){
return null;
}
String username = (String) tokenInfo.get("username");
String[] roles = (String[]) tokenInfo.get("roles");
ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(String role:roles){
authorities.add(new SimpleGrantedAuthority(role));
}
return new UsernamePasswordAuthenticationToken(username,null,authorities);
}
}
5.3.2 主类配置中添加认证过滤器bean
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Resource
UserDetailsServiceImpl userDetailsService;
/**
* 自定义登录过滤器的配置
*
* @return
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public LoginFilter loginFilter(AuthenticationManager authenticationManager) {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/login");
loginFilter.setAuthenticationManager(authenticationManager);
loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
// loginFilter.setRememberMeServices(rememberMeServices());
return loginFilter;
}
/**
* 自定义 Jwt 过滤器配置
*
* @param authenticationManager
* @return
*/
@Bean
public JWTAuthorizationFilter jwtAuthorizationFilter(AuthenticationManager authenticationManager) {
JWTAuthorizationFilter filter = new JWTAuthorizationFilter(authenticationManager);
return filter;
}
/**
* 鉴权、授权
*
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
// 特定权限访问页
.mvcMatchers("/user/**").hasRole("ADMIN")
.anyRequest().authenticated()
// 开启登录功能
.and()
.addFilterAt(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class)
.userDetailsService(userDetailsService)
.formLogin()
// 开启注销功能
.and()
.logout()
.logoutSuccessHandler(new MyLogoutSuccessHandler())
// 开启自定义 Jwt 过滤器
.and()
.addFilter(jwtAuthorizationFilter(http.getSharedObject(AuthenticationManager.class)))
// 关闭 csrf 和 sessionManagement
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
}
6 配置跨域
6.1 Spring 配置跨域的几种形式
6.1.1 使用 @CrossOrigin 注解
Spring 中第一种处理跨域的方式是通过@CrossOrigin 注解来标记支持跨域,该注解可以添加在方法上,也可以添加在 Controller 上。当添加在 Controller 上时,表示 Controller 中的所
有接口都支持跨域,具体配置如下:
@RestController
public Class HelloController{
@CrossOrigin (origins ="http://localhost:8081")
@PostMapping ("/post")
public String post (){
return "hello post";
}
}
@CrossOrigin 注解各属性含义如下:
-
alowCredentials:浏览器是否应当发送凭证信息,如 Cookie。
-
allowedHeaders: 请求被允许的请求头字段,
*
表示所有字段。 -
exposedHeaders:哪些响应头可以作为响应的一部分暴露出来。
注意,这里只可以一一列举,通配符 * 在这里是无效的。
-
maxAge:预检请求的有效期,有效期内不必再次发送预检请求,默认是
1800
秒。 -
methods:允许的请求方法,
*
表示允许所有方法。 -
origins:允许的域,
*
表示允许所有域。
6.1.2 在 MVC 中配置 addCrosMapping
@CrossOrigin 注解需要添加在不同的 Controller 上。所以还有一种全局配置方法,就是通过重写 WebMvcConfigurerComposite#addCorsMappings方法来实现,具体配置如下:
Configuration
public class WebMvcConfig implements WebMvcConfigurer{
Override
public void addCorsMappings (CorsRegistry registry){
registry.addMapping("/**") //处理的请求地址
.allowedMethods ("*")
•allowedorigins("*")
.allowedHeaders ("*")
.allowCredentials (false)
•exposedHeaders ("")
.maxAge (3600) ;
}
}
6.1.3 使用 CrosFilter 过滤器
Cosr Filter 是Spring Web 中提供的一个处理跨域的过滤器,开发者也可以通过该过该过滤器处理跨域。
@Configuration
public class WebMvcConfig {
@Bean
FilterRegistrationBean<CorsFilter> corsFilter() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
registrationBean.setFilter(new CorsFilter(source));
registrationBean.setOrder(-1);//filter 0 1
return registrationBean;
}
}
6.2 集成 Spring Security 后配置跨域
实际上我们配置的跨域也是一个过滤器,只是 Spring Security 帮我们把它放在了它在过滤器链中应该在的地方
@EnableWebSecurity
@Configuration
public class SecurityConfig {
/**
* Cors 跨域配置
*
* @return
*/
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
/**
* 鉴权、授权
*
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
// 解决跨域问题
.and()
.cors()
.configurationSource(configurationSource())
// 关闭 csrf 和 sessionManagement
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
}
7 两种异常的处理
我们可以对认证中常有的两种异常自定义其返回的数据
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 开启异常处理
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, e) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("message", "未认证,请登录");
result.put("data", null);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
})
.accessDeniedHandler((request, response, e) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 403);
result.put("message", "权限不足");
result.put("data", null);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
})
.and()
.build();
}
}
8 权限表达式
方法 | 说明 |
---|---|
hasAuthority(String authority) | 当前用户是否具备指定权限 |
hasAnyAuthority(String… authorities) | 当前用户是否具备指定权限中任意一个 |
hasRole(String role) | 当前用户是否具备指定角色 |
hasAnyRole(String… roles) | 当前用户是否具备指定角色中任意一个 |
permitAll() | 放行所有请求/调用 |
denyAll() | 拒绝所有请求/调用 |
isAnonymous() | 当前用户是否是一个匿名用户 |
isAuthenticated() | 当前用户是否已经认证成功 |
isRememberMe() | 当前用户是否通过 Remember-Me 自动登录 |
isFullyAuthenticated() | 当前用户是否既不是匿名用户又不是通过 Remember-Me 自动登录的 |
hasPermission(Object targetId, Object permission) | 当前用户是否具备指定目标的指定权限信息 |
hasPermission(Object targetId, String targetType, Object permission) | 当前用户是否具备指定目标的指定权限信息 |
9 最终配置文件
@EnableWebSecurity
@Configuration
public class SecurityConfig {
// @Resource
// private DataSource dataSource;
@Resource
UserDetailsServiceImpl userDetailsService;
/**
* 自定义 remember-me 的实现
* 使用 jwt 不再需要
* @return
*/
// @Bean
// public PersistentTokenRepository persistentTokenRepository() {
// JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// // 只需要没有表时设置为 true
// jdbcTokenRepository.setCreateTableOnStartup(false);
// jdbcTokenRepository.setDataSource(dataSource);
// return jdbcTokenRepository;
// }
//
// @Bean
// public RememberMeServices rememberMeServices() {
// return new PersistentTokenBasedRememberMeServicesImpl(UUID.randomUUID().toString(), userDetailsService, persistentTokenRepository());
// }
/**
* 自定义登录过滤器的配置
*
* @return
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public LoginFilter loginFilter(AuthenticationManager authenticationManager) {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/login");
loginFilter.setAuthenticationManager(authenticationManager);
loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
// loginFilter.setRememberMeServices(rememberMeServices());
return loginFilter;
}
/**
* Cors 跨域配置
*
* @return
*/
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
/**
* 自定义 Jwt 过滤器配置
*
* @param authenticationManager
* @return
*/
@Bean
public JWTAuthorizationFilter jwtAuthorizationFilter(AuthenticationManager authenticationManager) {
JWTAuthorizationFilter filter = new JWTAuthorizationFilter(authenticationManager);
return filter;
}
/**
* 鉴权、授权
*
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
// 接口文档所有人可以访问
.mvcMatchers("/swagger/**").permitAll()
.mvcMatchers("/swagger-ui.html").permitAll()
.mvcMatchers("/webjars/**").permitAll()
.mvcMatchers("/v2/**").permitAll()
.mvcMatchers("/v2/api-docs-ext/**").permitAll()
.mvcMatchers("/swagger-resources/**").permitAll()
.mvcMatchers("/doc.html").permitAll()
.mvcMatchers("/dataManage/**").permitAll()
.mvcMatchers("/menu/**").permitAll()
// 特定权限访问页
.mvcMatchers("/user/**").hasRole("ADMIN")
.anyRequest().authenticated()
// 开启登录功能
.and()
.addFilterAt(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class)
.userDetailsService(userDetailsService)
.formLogin()
// 开启 remember me 功能
// .and()
// .rememberMe()
// .rememberMeServices(rememberMeServices())
// .tokenRepository(persistentTokenRepository())
// .tokenValiditySeconds(10)
// 开启注销功能
.and()
.logout()
.logoutSuccessHandler(new MyLogoutSuccessHandler())
// 解决跨域问题
.and()
.cors()
.configurationSource(configurationSource())
// 开启异常处理
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, e) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("message", "未认证,请登录");
result.put("data", null);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
})
.accessDeniedHandler((request, response, e) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 403);
result.put("message", "权限不足");
result.put("data", null);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
})
// 开启自定义 Jwt 过滤器
.and()
.addFilter(jwtAuthorizationFilter(http.getSharedObject(AuthenticationManager.class)))
// 关闭 csrf 和 sessionManagement
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
}