springboot嘟嘟程序猿javaWeb学习

SpringBoot--实战开发--Spring Securit

2019-07-14  本文已影响18人  无剑_君

一、Spring Security简介

Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
网址:https://spring.io/projects/spring-security
在线文档

认证过程
用户使用用户名和密码进行登录。
Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。

二、Maven依赖

        <!--spring security-->
        <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>

三、测试

  1. 创建控制器
@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping
    public String getUsers() {
        return "Spring Security 测试";
    }
}
  1. 访问接口


    访问接口

    默认用户:user


    密码
结果
  1. 禁用安全设置或者设置对应的用户和密码
    在main方法上配置:
@SpringBootApplication(exclude = {
        org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class
})
  1. 配置文件中指定用户名与密码
spring.security.user.name=admin
spring.security.user.password=123456

四、自定义用户认证逻辑

  1. 添加配置类
@Configuration
public class SecurityConfig  extends WebSecurityConfigurerAdapter {
    @Bean
    UserDetailsService customUserService() {
        return new CustomUserDetailsService();
    }
    // 配置PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单登录
                .and()
                .authorizeRequests() // 对请求做授权, 定义哪些URL需要被保护、哪些不需要被保护
                .anyRequest() // 任何请求,登录后可以访问
                .authenticated() // 都需要身份认证
                .and().csrf().disable(); // 暂时将防护跨站请求伪造的功能置为不可用
    }
}

加密算法 PasswordEncoder 实现类
plaintext PlaintextPasswordEncoder
sha ShaPasswordEncoder
sha-256 ShaPasswordEncoder,使用时new ShaPasswordEncoder(256)
md4 Md4PasswordEncoder
md5 Md5PasswordEncoder
{sha} LdapShaPasswordEncoder
{ssha} LdapShaPasswordEncoder
  1. 添加自定义用户类
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
    // 自带加密类
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("登录用户名:"+username);
        // 根据用户名查找用户信息
        /**
         * 简单的对123456进行了加密的处理。我们可以进行测试,
         * 发现每次打印出来的password都是不一样的,这就是配置的BCryptPasswordEncoder所起到的作用。
         */
        String password = passwordEncoder.encode("123456");
        // 参数:1.账号 2.密码 3.账户是否可用(删除) 4.账户是否过期  5.密码是否过期  6.账户是否被锁定(冻结)7.角色
        return new User(username, password,
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

  1. 测试
    访问地址:http://localhost:8000/user
    认证
结果

五、单元测试

在写单元测试时,需要模拟某个用户的登录状态
在写单元测试时,需要模拟某个用户具有某个权限,但又不想改变数据库
编写单元测试时,需求完整调用某个用户的登录

@WithMockUser 模拟用户,手动指定用户名和授权
@WithAnonymousUser 模拟匿名用户
@WithUserDetails 模拟用户,给定用户名,通过自定义UserDetails来认证
@WithSecurityContext 通过SecurityContext构造器模拟用户

  1. 测试基类
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class BaseAdminApplicationTest {
    // 模拟MVC对象,通过MockMvcBuilders.webAppContextSetup(this.wac).build()初始化。
    public MockMvc mockMvc;
    // 注入WebApplicationContext
    @Autowired
    private WebApplicationContext wac;

    // 在测试开始前初始化工作
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).apply(springSecurity()).build();
    }
}

  1. 测试类
@Slf4j
public class AuthTest extends BaseAdminApplicationTest {
    /**
     * 1.登录测试
     *
     * @throws Exception
     */
    @Test
    public void testFormLoginSuccess() throws Exception {
        // 测试登录成功
        mockMvc
                .perform(formLogin("/login").user("admin").password("123456"))
                .andExpect(authenticated());
    }

    /**
     * 2. 测试登录失败
     *
     * @throws Exception
     */
    @Test
    public void testFormLoginFail() throws Exception {
        mockMvc
                .perform(formLogin("/login").user("admin").password("invalid"))
                .andExpect(unauthenticated());
    }

    /**
     * 测试退出登录
     *
     * @throws Exception
     */
    @Test
    public void testLogoutFail() throws Exception {
        // 测试退出登录
        mockMvc.perform(logout("/logout")).andExpect(unauthenticated());
    }
}

六、自定义登录认证

1.JWT依赖

  <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
  </dependency>

1. 创建JWT用户类

@Data
public class JwtUser implements UserDetails {

    private Long id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser() {
    }

    // 写一个能直接使用user创建jwtUser的构造器
    public JwtUser(User user) {
        id = user.getId();
        username = user.getUsername();
        password = user.getPassword();
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String toString() {
        return "JwtUser{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

2. 创建登录用户类

作用:获取表单登录信息与数据库实体不一定对应。

@Data
public class LoginUser {
    private String username;
    private String password;
    private Integer rememberMe;
}

3. 数据库用户类

@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String role;
}

4. JWT工具类

@Slf4j
public class JwtTokenUtils {
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";

    private static  String SECRET = "xxxxxxxx";
    private static final String ISS = "echisan";

    // 角色的key
    private static final String ROLE_CLAIMS = "role";

    // 过期时间是3600秒,既是1个小时
    private static final long EXPIRATION = 60 * 60L;

    // 选择了记住我之后的过期时间为7天
    private static final long EXPIRATION_REMEMBER = 7 * 24 * 60 * 60L;

    /**
     * 创建token
     *
     * @param username
     * @param role
     * @param isRememberMe
     * @return
     */
    public static String createToken(String username, String role, boolean isRememberMe,String secret) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        HashMap<String, Object> map = new HashMap<>();
        if(!secret.isEmpty()){
            SECRET=secret;
        }
        map.put(ROLE_CLAIMS, role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }

    /**
     * 从token中获取用户名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        return getTokenBody(token).getSubject();
    }

    /**
     * 获取用户角色
     *
     * @param token
     * @return
     */
    public static String getUserRole(String token) {
        return (String) getTokenBody(token).get(ROLE_CLAIMS);
    }

    /**
     * 是否已过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        return getTokenBody(token).getExpiration().before(new Date());
    }

    /**
     * 获取令牌体
     *
     * @param token
     * @return
     */
    private static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

5. 创建认证过滤器

@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private ThreadLocal<Integer> rememberMe = new ThreadLocal<>();
    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // 设置过滤器处理地址
        super.setFilterProcessesUrl("/auth/login");
    }

    /**
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try {
            // 获取登录信息
            LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
             log.info("登录用户:"+loginUser.getUsername()+"密码:"+loginUser.getPassword());
            // 记住登录信息
            rememberMe.set(loginUser.getRememberMe());
            // 进行认证
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     *  成功验证后调用的方法
     *  如果验证成功,就生成token并返回
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 获取Jwt用户信息
        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("用户:" + jwtUser.toString()+"密码:"+jwtUser.getPassword());
        // 记住登录信息
        boolean isRemember = rememberMe.get() == 1;
        // 获取授权信息
        String role = "";
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            role = authority.getAuthority();
        }
        // 创建令牌
        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember,jwtUser.getPassword());
//        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
        // 返回创建成功的token
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的时候应该是 `Bearer token`
        response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.getWriter().write("认证失败, 原因: " + failed.getMessage());
    }
}

6. 创建授权过滤器

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        // 获取请求头信息
        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 这里从token中获取用户信息并新建一个token
     * @param tokenHeader
     * @return
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        // 获取令牌
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        // 获取用户信息
        String username = JwtTokenUtils.getUsername(token);
        // 获取角色信息
        String role = JwtTokenUtils.getUserRole(token);
        if (username != null){
            return new UsernamePasswordAuthenticationToken(username, null,
                    Collections.singleton(new SimpleGrantedAuthority(role))
            );
        }
        return null;
    }
}
  1. 创建配置
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 设置加密方式
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    /**
     * 配置http权限认证
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 测试用资源,需要验证了的用户才能访问
                .antMatchers("/user/**").authenticated()
                .antMatchers(HttpMethod.DELETE, "/user/**").hasRole("ADMIN")
                // 其它的都可以访问
                .anyRequest().permitAll()
                .and()
                // 添加认证过滤器
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                // 添加授权过滤器
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 添加异常处理
                .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
    }

    /**
     * 跨域请求配置
     * @return
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}
  1. 统一认证异常处理
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        String reason = "认证失败,原因:"+authException.getMessage();
        response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
    }
}
  1. 用户业务类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      // 通过username在数据库中查找用户,这里模拟
        User user=new User();
        user.setUsername("admin");
        user.setPassword(bCryptPasswordEncoder.encode("123456"));
        user.setId(1L);
        user.setRole("admin");
        return new JwtUser(user);
    }

}
  1. 控制器类
    验证控制器:
@RestController
@RequestMapping("/auth")
@Slf4j
public class AuthController {
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    @PostMapping("/register")
    public String registerUser(@RequestBody Map<String,String> registerUser){
        User user = new User();
        user.setUsername(registerUser.get("username"));
        user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
        user.setRole("ROLE_USER");
        return "注册成功";
    }
}

用户控制器

@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping
    public String getUsers() {
        return "Spring Security 测试";
    }
}

七、认证测试

  1. 获取令牌
    localhost:8000/auth/login


    获取令牌
  2. 资源访问
    localhost:8000/user


    资源访问

八、JWTToken超时刷新

九、spring security退出功能

注销默认地址:/logout
spring security实现注销功能涉及的三个核心类为LogoutFilter,LogoutHandler,LogoutSuccessHandler
LoginFilter是实现注销功能的过滤器,默认拦截/logout或者logout属性logout-url指定的url
LogoutHandler接口定义了退出登录操作的方法
LogoutSuccessHandler接口定义了注销之后的操作方法
登出处理器:

@Component
@Slf4j
public class LogoutAuthenticationHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        // TODO 注意:要想在此获取authentication数据,登录成功后,必须调用一次资源,才可获取。
        // 与 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 等同
        log.info(authentication.getPrincipal() + "");
        // 在此可以删除redis中保存的token数据
        log.info("注销成功");
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString("ok"));
    }
}

十、授权表达式

表达式 说明
permitAll 永远返回 true
denyAll 永远返回 false
anonyous 当前用户若是匿名用户返回 true
rememberMe 当前用户若是 rememberMe 用户返回 true
authenticated 当前用户若不是匿名(已认证)用户返回 true
fullAuthenticated 当前用户若既不是匿名用户又不是 rememberMe 用户时返回 true
hasRole(role) 当前用户权限集合中若拥有指定的 role 角色权限(匹配时会在你所指定的权限前加'ROLE_',即判断是否有“ROLE_role”权限)时返回 true
hasAnyRole(role1, role2, ...) 当前用户权限集合中若拥有任意一个角色权限时返回 true
hasAuthority(authority) 当前用户权限集合中若具有 authority 权限(匹配是否有“authority”权限)时返回 true
hasAnyAuthority(authority) 当前用户权限集合中若拥有任意一个权限时返回 true
hasIpAddress("192.168.1.0/24") 发送请求的IP匹配时fanhui true

基于角色的访问控制 RBAC 数据模型(Role-Based Access Control)

十一、Spring Security过滤器链及认证流程

spring Security功能的实现主要是由一系列过滤器链相互配合完成。


Spring Security过滤器链及认证流程

过滤器链中主要的几个过滤器及其作用:
1.SecurityContextPersistenceFilter 会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
2.UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
3.FilterSecurityInterceptor 是用于保护Http 资源的,它需要一个AccessDecisionManager和一个AuthenticationManager 的引用。它会从 SecurityContextHolder 获取 Authentication,然后通过 SecurityMetadataSource 可以得知当前请求是否在请求受保护的资源。对于请求那些受保护的资源,如果Authentication.isAuthenticated()返回false或者FilterSecurityInterceptor
的alwaysReauthenticate 属性为 true,那么将会使用其引用的 AuthenticationManager 再认证一次,认证之后再使用认证后的 Authentication 替换 SecurityContextHolder 中拥有的那个。然后就是利用 AccessDecisionManager 进行权限的检查;
4.ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。
但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
--- 如果捕获到的是 AuthenticationException,那么将会使用其对应的 AuthenticationEntryPoint 的commence()处理。在处理之前,ExceptionTranslationFilter先使用 RequestCache 将当前的HttpServerletRequest的信息保存起来,以至于用户成功登录后可以跳转到之前的界面;
--- 如果捕获到的是 AccessDeniedException,那么将视当前访问的用户是否已经登录认证做不同的处理,如果未登录,则会使用关联的 AuthenticationEntryPoint 的 commence()方法进行处理,否则将使用关联的 AccessDeniedHandler 的handle()方法进行处理。

十二、认证流程

认证流程分为登录流程和注销流程:
(注意:这里的登录方式为"帐号+密码";箭头代表整个流程执行方向)
1)登录流程
---> SecurityContextPersistenceFilter(作用在第一节已经介绍)
---> UsernamePasswordAuthenticationFilter
(先获取用户名和密码,并将其封装成UsernamePasswordToken,然后调用AuthenticationManager进行验证)
---> AuthenticationManager
(根据token类型选择合适的AuthenticationProvider来处理认证请求) [默认实现类:ProviderManager]
AuthenticationManager是一个用来处理请求的接口,它自己不直接处理认证请求,而是委托给其所配置的Authentication
Provider列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个AuthenticationProvider 认证后的结果不为 null,则表示该AuthenticationProvider已经认证成功,之后的AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为null,则表示认证失败,将抛出一个 ProviderNotFoundException。
---> AuthenticationProvider
(请求认证处理) [默认实现类:DaoAuthencationProvider]
DaoAuthenticationProvider认证过程:
DaoAuthenticationProvider先调用UserDetailsService 的loadUserByUsername()方法获取UserDetails,获取后再与UsernamePasswordAuthenticationFilter获取的username和password进行比较;如果认证通过后会将该 UserDetails 赋给认证通过的 Authentication的principal,然后再把该 Authentication 存入到 SecurityContext 中。默认情况下,在认证成功后ProviderManager也将清除返回的Authentication中的凭证信息。
注意:在这里面根据需要增加[自定义关键类(UserDetailService):实现UserDetailService接口并复写loadUserByUsername()]
问:为什么AuthenticationManager不直接认证请求?
答:因为token有多种类型。比如最简单的UsernamePasswordAuthenticationToken,还有spring social的token;
---> Authentication对象
Spring Security使用一个Authentication 对象来描述当前用户的相关信息。SecurityContextHolder中持有的是当前用户的 SecurityContext,而 SecurityContext 持有的是代表当前用户相关信息的 Authentication 的引用。这个 Authentication 对象不需要我们自己去创建,在与系统交互的过程中,Spring Security会自动为我们创建相应的Authentication对象,然后赋值给当前的SecurityContext。
2)注销流程
1、使HttpSession失效;
2、清除所有已经配置的remember-me认证;
3、清除SecurityContextHolder中的user信息,并设置Authentication中的Authenticated属性为false;
4、跳转到指定url;

附:
认证提供:

/**
 * @描 述:
 *  AuthenticationProvider(身份验证提供者) 顾名思义,可以提供一个 Authentication 供Spring Security的上下文使用
 *  1. 创建CustomAuthenticationProvider类
 *  2. 当 CustomAuthenticationProvider 认证成功之后,JWTLoginFilter 中的 successfulAuthentication() 方法机会执行
 *  本类没有用到
 */
@Slf4j
public class CustomAuthenticationProvider  implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    /**
     *  验证登录信息,若登陆成功,设置 Authentication
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        //通过用户名从数据库中查询该用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        //判断密码是否正确
        String dbPassword = userDetails.getPassword();
        String encoderPassword = bCryptPasswordEncoder.encode(password);
        if (!dbPassword.equals(encoderPassword)) {
//            throw new UsernameIsExitedException("密码错误");
            log.info("密码错误");
        }
        Authentication auth = new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
        return auth;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 进行验证
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

十三、常见问题:

  1. Encoded password does not look like BCrypt
    添加配置:
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());

    }
  1. Spring Security拦截器引起Java CORS跨域失败的问题
    response响应头:
响应头字段名称 作用
Access-Control-Allow-Origin 允许访问的客户端的域名
Access-Control-Allow-Credentials 是否允许请求带有验证信息,若要获取客户端域下的cookie时,需要将其设置为true。
Access-Control-Allow-Headers 允许服务端访问的客户端请求头
Access-Control-Allow-Methods 允许访问的HTTP请求方法
Access-Control-Max-Age 用来指定预检请求的有效期(秒),在有效期内不在发送预检请求直接请求。

解决:

@Configuration
public class WebAppConfigurer extends WebMvcConfigurationSupport {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}
上一篇下一篇

猜你喜欢

热点阅读