Spring Security 5.7 的简单搭建流程

1 环境搭建

1.1 pom 文件中依赖的引入

该流程搭建的是一个前后端分离的项目,环境搭建使用 Spring boot 2.7.10Spring Security 5.7,数据库使用 MySQL8druidMyBatis-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,在我们自己定义的过滤器中我们需要做几件事:

  1. 判断前端提交的请求是否是 POST 方式
  2. 获取前端传来的 josn 并解析得到用户名和密码
  3. 将用户名和密码交给 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 接口的几个类就可以实现对密码的加密,也可以自己实现这个接口来自定义密码加密方式。

默认提供加密算法如下:

spring security6学习 spring security5_java

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