学习 Spring Security 登录认证 

        学习在 JAVA 开发中如何使用 Spring Security 做登录认证

        注意:下方代码中的注释为重点

 1.介绍

  • Spring Security是 Spring 全家桶中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比 Shiro 丰富活跃;
  • 一般情况分布式开发和一些大型项目会选择使用 SpringSecurity ,对于一些中小型的单体项目多数会选择更加容易上手的 Shiro 或者 JWT + Token 使用 WebMvcConfigurer 去进行认证登录。
  • 一般Web应用的需求就是进行 认证 和 授权 
  • 认证:验证当前访问系统的用户是否为本系统的用户,并且要确认具体是哪个用户。
  • 授权:经过认证后判断当前用户所拥有的权限,或者是否有权限进行某个操作。

认证 和 授权 也是 SpringSecurity 安全框架的核心功能。

官网链接:Spring Security :: Spring Security

 2.快速入门

2-1.准备工作

创建一个SpringBoot项目,这里就不过多介绍了

springsecurity 验证码登录 spring security 登录_spring boot

2-1-1.pom.xml 文件
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.22</version>
        </dependency>
        <!--        数据源-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--        jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
2-1-2.配置数据源 yml 文件
server:
  port: 5210

spring:
  application:
    name: spring-security

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_name?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: 数据库用户名
    password: 数据库密码
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 6000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 30000
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20

  redis:
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器端口号
    port: 6379
    # 连接超时时间
    timeout: 1800000
    # 设置密码
    password: root
    # 使用的数据库索引,默认是0
    database: 0
    lettuce:
      pool:
        # 最大阻塞等待时间,负数表示没有限制
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 5
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中最大连接数,负数表示没有限制
        max-active: 20


mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  typeAliasesPackage: com.pojo
  configuration:
    # 驼峰命名
    map-underscore-to-camel-case: true
    cache-enabled: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志
  global-config:
    db-config:
      column-underline: true
      id-type: auto
2-1-3自定义 jwt 工具类
@Slf4j
public class JwtUtil {
    private static final long time = 1000 * 60 * 60 * 24 * 7;//有效期7天
    private static final String key = "java_utils_jwt";
    private static SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    /**
     * 生成密钥
     */
    public static SecretKey getKey() {
        //自定义 key 的编码 采用 base64 编码加密
        String encode = new BASE64Encoder().encode(key.getBytes());
        //根据key生成签名密钥
        byte[] bytes = DatatypeConverter.parseBase64Binary(encode);
        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, signatureAlgorithm.getJcaName());
        return secretKeySpec;
    }

    /**
     * 生成 token
     */
    public static String getToken(Integer id) {
        return Jwts.builder()
                .setHeaderParam("Type", "JWT")
                .setId(UUID.randomUUID().toString())
                .setSubject("user_login")
                .claim("userId", id)
                .setExpiration(new Date(System.currentTimeMillis() + time))
                .signWith(signatureAlgorithm, JwtUtil.getKey())
                .compact();
    }

    /**
     * 解析 token
     */
    public static Claims parserToken(String token) {
        byte[] bytes = DatatypeConverter.parseBase64Binary(new BASE64Encoder().encode(key.getBytes()));
        return Jwts.parser()
                .setSigningKey(bytes)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 获取 token 中的用户信息
     */
    public static Integer getUserId(String token) {
        Claims claims = parserToken(token);
        Integer id = (Integer) claims.get("userId");
        return id;
    }
}
2-1-4.自定义RedisConfig类
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();

        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }


}
2-1-5.RedisUtils类
public class RedisUtils {

    /* 登录用户token值 */
    public static final String LOGIN_USER_ID = "login_user_id_";
    public static final long TOKEN_TIMES = 4;//小时
    public static final long TOKEN_LONG_TIME = 7;//天


}
2-1-6.User实体类
@Data
@TableName("java_user")
public class User {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 登录名
     */
    private String loginName;
    /**
     * 登录密码(明文)
     */
    private String loginPwd;
    /**
     * 角色id
     */
    private Integer roleId;
    /**
     * 密文
     */
    private String password;
    /**
     * 姓名
     */
    private String name;

    /**
     * 联系方式
     */
    private String phone;
    /**
     * 身份证号
     */
    private String idCard;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别: 0.女 1.男
     */
    private Integer sex;
    /**
     * 是否启用: 0.未启用  1.启用
     */
    private Integer isEnable;
    /**
     * 创建时间
     */
    private Date insDate;
    /**
     * 修改时间
     */
    private Date updDate;
    /**
     * 是否删除:0.未删除 1.删除
     */
    @TableLogic(value = "0",delval = "1")
    private Integer isDel;


    /*
     * 非表字段
     * */
    @TableField(exist = false)
    private String title;
    @TableField(exist = false)
    private String token;
}

2-2.启动项目测试

springsecurity 验证码登录 spring security 登录_mysql_02

2-3.开始Security

2-3-1.添加 SpringSecurity 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

如果SpringBoot是2.5.x版本,那么Spring Security的版本是5.5

如果SpringBoot是2.7.x版本,那么Spring Security的版本是5.7

2-3-2.重新启动测试

springsecurity 验证码登录 spring security 登录_spring_03

这里可以看到我们重新访问刚才的 /login/test 路径会自动跳转到登录页面,

默认用户名:user  ,密码在启动项目的控制台中:

springsecurity 验证码登录 spring security 登录_mysql_04

登录后就会重新访问到我们的接口并返回数据:

springsecurity 验证码登录 spring security 登录_java_05

2-3-3.退出登录

访问 http://localhost:5210/logout

springsecurity 验证码登录 spring security 登录_redis_06

点击上面的 Log Out 按钮则会退出登录,重新访问刚才的测试接口就会从新回到登录页面进行登录认证功能。

2-4.认证流程

springsecurity 验证码登录 spring security 登录_java_07

系统中肯定会存在大量的用户信息,security如何确认当前登录系统的是哪一个用户呢?

Authentication: 存储了认证信息,代表当前登录用户。

使用方法:

        通过 SecurityContext 来获取 AuthenticationSecurityContext就是我们的上下文对象,SecurityContext 对象是交给 SecurityContextHolder

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

原理:使用 ThreadLocal 线程来保证传递同一个对象。

springsecurity 验证码登录 spring security 登录_java_08

  • UsernamePasswordAuthenticationFilter:是最常用的用户名和密码认证方式的主要处理类,构造了一个UsernamePasswordAuthenticationToken对象实现类,将用请求信息封装为Authentication
  • BasicAuthenticationFilter:将UsernamePasswordAuthenticationToken封装成的 Authentication进行登录逻辑处理
  • ExceptionTranslationFilter:主要处理认证和授权的异常
  • FilterSecurityInterceptor:负责权限校验
  • 等。。。

参考地址: Spring Security Reference

3.整合mysql、redis、jwt 进行登录认证 - 核心代码

3-1.实现 UserDetailsService 类

1.创建一个类实现 UserDetailsService

2.重写 loadUserByUsername 方法

3.通过用户名从数据库中查询用户信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //(认证,即校验该用户是否存在)查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getLoginName,username);
        User user = userMapper.selectOne(queryWrapper);
        //如果没有查询到用户
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }


        // (授权,即查询用户具有哪些权限)查询对应的用户信息

        return new LoginUserDetails(user);
    }
}

因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

如果你想让用户的密码是明文存储,需要在密码前加 {noop}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserDetails implements UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLoginName();
    }

    //是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //凭证是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

3-2.密码加密存储

        在实际开发项目中明文密码一般不会存储在数据库中,默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder

        使用 BCryptPasswordEncoder 进行替换

        使用的时候把 BCryptPasswordEncoder 对象注入Spring容器中,SpringSecurity就会使用当前的 PasswordEncoder 来进行密码校验。

3-2-1.配置config类继承 WebSecurityConfigurerAdapter 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {

    /*
    * 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password ,它会根据id去判断密码的加密方式(一般不会采用这种方式),
    *   所以就需要替换PasswordEncoder,我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
    *
    * 只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
    * */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


}

3-3.登录接口

重点:下文放行登录接口一定要注意config文件的变化

  1. 登录接口一定要走security的过滤器连,在自定义的认证过滤器中进行访问路径判断放行方式。(因为整个过程中,还有其他事情要做。 登录成功之后,将用户用户数据存在 SecurityContextHolder 中 ,如果没走 Security 过滤器链 无法通过 SecurityContextHolder 获取到登录用户信息,因此其他的请求还是会被拦截)
  2. 静态资源采用白名单方式,重写 configure(WebSecurity web) 方法。(登录接口不可在这里配置)
3-3-1.配置 config

        在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。认证成功的话使用JWT生成一个token放在返回中,下次用户请求直接在头部信息中加入token一起请求可以通过JWT识别当前登录的用户。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //过滤请求
                .authorizeRequests()
                // 静态资源或者放行接口 允许匿名访问
                .antMatchers("/login/user_login").permitAll()
                //options 方法的请求放行
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();


    }


}
3-3-2.编写Controller 接口
@RestController
@RequestMapping("/login")
public class LoginController {
    
    @Resource
    private LoginService loginService;

    @PostMapping("/user_login")
    public Map<String,Object> user_login(@RequestBody LoginVo loginVo){
        return loginService.user_login(loginVo);
    }

}
3-3-3. LoginVo 传参实体类对象
@Data
public class LoginVo {
    private String loginName;
    private String loginPwd;
}
3-3-4. LoginServiceImpl 业务实现类
@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public Map<String,Object> user_login(LoginVo loginVo) {
        Map<String,Object> map = new HashMap<>();

        //通过UsernamePasswordAuthenticationToken获取用户名和密码
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getLoginName(), loginVo.getLoginPwd());
        //AuthenticationManager委托机制对authenticationToken 进行用户认证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //如果认证没有通过,给出对应的提示
        if (Objects.isNull(authenticate)) {
            map.put("err","用户名或密码错误");
        }

        //如果认证通过,使用user 通过 jwt 生成 token 返回token
        //拿到这个当前登录用户信息
        LoginUserDetails loginUserDetails = (LoginUserDetails) authenticate.getPrincipal();
        //获取当前用户的user
        User user = loginUserDetails.getUser();

        String token = JwtUtil.getToken(user.getId());
        user.setToken(token);

        //置空密码 : 我这边使用了保存明文密码,所以这边要隐藏明文密码,实际开发一般不选择保存明文密码
        user.setLoginPwd(null);

        //将完整的用户信息存入redis缓存
        redisTemplate.opsForValue().set(RedisUtils.LOGIN_USER_ID + user.getId(), user, RedisUtils.TOKEN_TIMES, TimeUnit.HOURS);

        map.put("code",200);
        map.put("msg","认证成功");
        map.put("token",token);

        return map;
    }
}
3-3-5.自定义认证过滤器 实现 OncePerRequestFilter
@Slf4j
@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //放行登录接口
        String requestURI = request.getRequestURI();
        if (requestURI.equals("/login/user_login")){
            filterChain.doFilter(request, response);
            return;
        }

        //获取token
        String token = request.getHeader("token");
        if (token == null || token.equals("")) {
            this.return_message(response, 501, "请登录!");
            return;
        } else {
            token = token.replace("Bearer ", "");
        }

        //解析token 获取userId
        Integer userId = null;
        try {
            userId = JwtUtil.getUserId(token);
        } catch (Exception e) {
            this.return_message(response, 501, "非法登录令牌!");
            return;
        }

        //从redis中获取用户信息
        User user = (User) redisTemplate.opsForValue().get(RedisUtils.LOGIN_USER_ID + userId);
        if (Objects.isNull(user)) {
            this.return_message(response, 501, "未登录!");
            return;
        }

        //封装Authentication对象存入SecurityContextHolder && 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //放行
        filterChain.doFilter(request, response);
    }

    /*
     * 返回信息
     * */
    public void return_message(HttpServletResponse response, Integer code, String msg) {
        Map<String,Object> map = new HashMap<>();
        map.put("code",code);
        map.put("msg",msg);
        String s = JSON.toJSONString(map);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
        } catch (IOException e) {
            log.info("信息返回异常:{}",e);
        }
        writer.write(s);
    }

}

 把token校验过滤器添加到过滤器链中

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Resource
    private AuthenticationTokenFilter authenticationTokenFilter;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //过滤请求
                .authorizeRequests()
                // 静态资源或者放行接口 允许匿名访问
                .antMatchers("/login/user_login").permitAll()
                //options 方法的请求放行
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


    }


}
3-3-6.放行登录接口
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /*
     * 静态资源
     * */
    String[] state_url = new String[]{
            "/favicon.ico",
    };

    /*
    * 白名单 -> HttpSecurity 中的 antMatchers(state_url).permitAll() 只是放行security过滤器链中的拦截
    *  WebSecurity -> mvc放行所有的拦截 : 不会再走security的过滤器链
    * */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(state_url);
    }

    @Resource
    private AuthenticationTokenFilter authenticationTokenFilter;

    /*
    * 注意: /login/user_login 登录接口必须要走 Security 过滤器链,因为在这个过程中,还有其他事情要做。 登录成功之后,将用户用户数据存在 SecurityContextHolder 中
    *   如果没走 Security 过滤器链 无法通过 SecurityContextHolder 获取到登录用户信息,因此请求还是会被拦截
    * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //过滤请求
                .authorizeRequests()
                // 静态资源或者放行接口 允许匿名访问
                .antMatchers("/login/user_login").permitAll()
                //options 方法的请求放行
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }



}

3-4.退出登录

        只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

@PostMapping("/user_login_out")
    public Map<String,Object> user_login_out(){
        return loginService.user_login_out();
    }
@Override
    public Map<String, Object> user_login_out() {
        //从SecurityContextHolder中的userid
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user= (User) authentication.getPrincipal();
        //从缓存中删除当前用户登录信息
        redisTemplate.delete(RedisUtils.LOGIN_USER_ID + user.getId());

        Map<String, Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","退出成功");
        return map;
    }

4.测试认证

我这边测试使用测试工具 Postman

1.测试未登录状态访问 /login/test

springsecurity 验证码登录 spring security 登录_java_09

2.测试登录接口(明文密码测试认证失败的去查看下面的解决异常里面)

springsecurity 验证码登录 spring security 登录_mysql_10

3.登录状态下访问 /login/test

springsecurity 验证码登录 spring security 登录_spring boot_11

4.测试退出登录

springsecurity 验证码登录 spring security 登录_redis_12

5.授权

         授权功能请参考:Spring Security 授权-CSDN博客

0.解决异常

0-1.解决跨域问题

        浏览器出于安全的考虑,发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

        前后端分离开发项目中一般是不同源的 (域名,端口...不一致)产生跨域问题

 我这边曹勇同一个 config 进行配置,也可将其分开。

0-1-1.配置上文中的  SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {

    //此处为上文中 Security 的配置,省略。。。

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //允许跨域请求的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOrigins("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("*")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }

}
 0-1-2.开启SpringSecurity的跨域访问
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //过滤请求
                .authorizeRequests()
                // 静态资源或者放行接口 允许匿名访问
                .antMatchers("/login/user_login").permitAll()
                //options 方法的请求放行
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //允许跨域(开启跨域)
        http.cors();
    }

}

0-2.明文密码认证失败

数据库保存明文密码,密码前面加 {noop} ,但是使用明文密码认证还是失败原因:

        在SpmingSecunty升级到新版本(SpringBoot2x)后,因为默以把之前的明文方式给去挂了所以在对授权客户端的密码进行加密时可能就会出现 Encoded password does not look like BCrypt 异常问题。

@Resource
    private UserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

此时数据库中的密码需要去掉 {noop} 

springsecurity 验证码登录 spring security 登录_spring_13

springsecurity 验证码登录 spring security 登录_java_14

注意:

        如果在config中加入上面的方法,则数据库保存的密码要一定是明文密码,此时数据库中保存的密码如果为加密后的密文,则会认证失败。想要换成密文存储方式就把上面config中加入的配置给注释掉或删掉。(并且此方法已经过时)

个人推荐使用密文存储方式。