Spring Security

Spring Security

一、介绍

​ Spring Security是一个功能强大且高度可定制的身份验证(authentication)和权限控制(access-control)框架。它是用于保护Spring的应用程序。Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security可以非常容易地扩展以满足定制需求。

二、简单使用

导入starter:

<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>

编写测试接口:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello spring security";
    }
}

在这里插入图片描述

进行登录,用户名默认为user,密码为随机UUID值在控制台输出

在这里插入图片描述

认证之后请求/hello访问成功

在这里插入图片描述

三、默认配置分析

导入Spring Security依赖之后,即可完成简单的认证,分析以下问题:

1.未认证时跳转到登录页面

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

DefaultLoginPageGeneratingFilter(默认登录页生成过滤器)

在这里插入图片描述

在这里插入图片描述

2.登录页面从何而来

由字符串拼接成登录页面以Response对象返回

//按照认证方式生成登录页
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
    
    ...

    String contextPath = request.getContextPath();
    StringBuilder sb = new StringBuilder();
    sb.append("<!DOCTYPE html>\n");
    sb.append("<html lang=\"en\">\n");
    sb.append("  <head>\n");
    sb.append("    <meta charset=\"utf-8\">\n");
    sb.append("    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
    sb.append("    <meta name=\"description\" content=\"\">\n");
    sb.append("    <meta name=\"author\" content=\"\">\n");
    sb.append("    <title>Please sign in</title>\n");
    sb.append("    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
    sb.append("    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
    sb.append("  </head>\n");
    sb.append("  <body>\n");
    sb.append("     <div class=\"container\">\n");

    //其他认证方式
    ...
        
    //如果是表单认证
    if (this.formLoginEnabled) {
            sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n");
            sb.append("        <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
            sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n");
            sb.append("          <label for=\"username\" class=\"sr-only\">Username</label>\n");
            sb.append("          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
            sb.append("        </p>\n");
            sb.append("        <p>\n");
            sb.append("          <label for=\"password\" class=\"sr-only\">Password</label>\n");
            sb.append("          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n");
            sb.append("        </p>\n");
            sb.append(this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request));
            sb.append("        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
            sb.append("      </form>\n");
        }
    
    //其他认证方式
    ...
        
    sb.append("</div>\n");
    sb.append("</body></html>");
    return sb.toString();
 }

在这里插入图片描述

3.登录用户名密码在哪里存储

由于Spring Security默认进行formLogin表单登录认证

在这里插入图片描述

在这里插入图片描述

UsernamePasswordAuthenticationFilter中的attemptAuthentication方法:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

//ProviderManager中的方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    int currentPosition = 0;
    int size = this.providers.size();
    Iterator var9 = this.getProviders().iterator();

    while(var9.hasNext()) {
        //获取当前ProviderManager中的每一个AuthenticationProvider
        AuthenticationProvider provider = (AuthenticationProvider)var9.next();
        //看是否支持认证
        if (provider.supports(toTest)) {
            if (logger.isTraceEnabled()) {
                Log var10000 = logger;
                String var10002 = provider.getClass().getSimpleName();
                ++currentPosition;
                var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
            }

            try {
                //调用AuthenticationProvider进行真正的认证
                result = provider.authenticate(authentication);
                if (result != null) {
                    this.copyDetails(authentication, result);
                    break;
                }
            } catch (InternalAuthenticationServiceException | AccountStatusException var14) {
                this.prepareException(var14, authentication);
                throw var14;
            } catch (AuthenticationException var15) {
                lastException = var15;
            }
        }
    }

    //如果当前的ProviderManager中的所有AuthenticationProvider都不支持认证
    if (result == null && this.parent != null) {
        try {
            //交给它的父亲(全局的AuthenticationManager)去认证
            parentResult = this.parent.authenticate(authentication);
            result = parentResult;
        } catch (ProviderNotFoundException var12) {
        } catch (AuthenticationException var13) {
            parentException = var13;
            lastException = var13;
        }
    }

    //如果有结果返回认证后的Authentication对象,否则抛出异常
    if (result != null) {
        if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
            ((CredentialsContainer)result).eraseCredentials();
        }

        if (parentResult == null) {
            this.eventPublisher.publishAuthenticationSuccess(result);
        }

        return result;
    } else {
        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
        }

        if (parentException == null) {
            this.prepareException((AuthenticationException)lastException, authentication);
        }

        throw lastException;
    }
}

在这里插入图片描述

交给全局的AuthenticationManager进行认证:

在这里插入图片描述

//AbstractUserDetailsAuthenticationProvider中的方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
        return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
    });
    
    //判断用户名是否为空
    String username = this.determineUsername(authentication);
    boolean cacheWasUsed = true;
    //尝试从缓存中获取用户信息进行比对
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;

        try {
		   //本类中retrieveUser方法为抽象方法,这里实际上是调用子类			       			    DaoAuthenticationProvider中重写的方法
            //通过用户名查找用户信息(验证用户名),用户名不存在抛出异常
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        } catch (UsernameNotFoundException var6) {
            this.logger.debug("Failed to find user '" + username + "'");
            if (!this.hideUserNotFoundExceptions) {
                throw var6;
            }

            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        //判断该用户是否启用、是否过期、是否锁定
        this.preAuthenticationChecks.check(user);
        //比较密码
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {
        if (!cacheWasUsed) {
            throw var7;
        }

        cacheWasUsed = false;
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    }

    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

//判断用户状态
public void check(UserDetails user) {
    if (!user.isAccountNonLocked()) {
        AbstractUserDetailsAuthenticationProvider.this.logger.debug("Failed to authenticate since user account is locked");
        throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
    } else if (!user.isEnabled()) {
        AbstractUserDetailsAuthenticationProvider.this.logger.debug("Failed to authenticate since user account is disabled");
        throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
    } else if (!user.isAccountNonExpired()) {
        AbstractUserDetailsAuthenticationProvider.this.logger.debug("Failed to authenticate since user account has expired");
        throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
    }
}
//DaoAuthenticationProvider类中方法
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    this.prepareTimingAttackProtection();

    try {
        //调用UserDetailsService的实现类的loadUserByUsername方法
        //通过用户名查找用户信息
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    } catch (UsernameNotFoundException var4) {
        this.mitigateAgainstTimingAttack(authentication);
        throw var4;
    } catch (InternalAuthenticationServiceException var5) {
        throw var5;
    } catch (Exception var6) {
        throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
    }
}

在这里插入图片描述

在这里插入图片描述

//InMemoryUserDetailsManager类中
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //通过用户名查找用户信息
    UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
    if (user == null) {
        throw new UsernameNotFoundException(username);
    } else {
        return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
    }
}

登录用户存储在内存之中,可以通过配置文件进行修改

在这里插入图片描述

# 修改默认登录用户名和密码
spring:
  security:
    user:
      name: root
      password: root

四、自定义认证

在这里插入图片描述

默认配置生效的条件:

在这里插入图片描述

在这里插入图片描述

因此我们只需要定义一个WebSecurityConfigurerAdapter让默认配置不生效
在这里插入图片描述

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //授权请求
        http.authorizeRequests()
            	//放行所有静态资源路径
                .mvcMatchers("/res/**").permitAll()
            	//放行登录请求
                .mvcMatchers("/login").permitAll()
            	//其他所有请求都需要认证
                .anyRequest().authenticated()
                .and()
                .formLogin()
            	//认证成功返回Json信息
			   .successHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "认证成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                //认证失败返回Json信息
                .failureHandler(((request, response, exception) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 401);
                    map.put("msg", "认证失败!!!");
                    map.put("exception", exception);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
            	//定义注销的请求方式和请求路径可以有多个
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
            	//注销成功返回Json信息
                .logoutSuccessHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
            	//关闭跨站请求访问
                .csrf().disable();
    }
}

五、自定义数据源

1.源码分析

在前面我们已经分析出:

AuthenticationManager的实现类ProviderManager中有一个AuthenticationProvider的集合,遍历其中

的每一个Provider判断是否支持认证,如果支持则调用Provider中的authenticate方法,

UserDetailsService中的loadUserByUsername()方法中获取用户信息
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

因此,我们可以模仿Spring Security编写的User对象:

  1. 自定义一个User类实现UserDetails接口

在这里插入图片描述

2.自定义一个UserService类实现UserDetailsService接口,重写loadUserByUsername方法

在这里插入图片描述

​ 3.将UserService配置到自定义的AuthenticationManager中

在这里插入图片描述

2.连接数据源

设计数据库表:

create table user
(
    id                  varchar(50)   not null comment '用户id'
        primary key,
    username            varchar(50)   not null comment '用户名',
    password            varchar(50)   not null comment '密码',
    deleted             tinyint           not null comment '逻辑删除字段',
    account_expired     int default 0 not null comment '用户是否过期',
    account_locked      int default 0 not null comment '用户是否锁定',
    credentials_expired int default 0 not null comment '密码是否过期',
    create_time         datetime      not null on update CURRENT_TIMESTAMP comment '创建时间',
    update_time         datetime      not null on update CURRENT_TIMESTAMP comment '修改时间',
    version             bigint           not null comment '乐观锁字段'
);

create table role
(
    id          int auto_increment comment '角色id'
        primary key,
    role        varchar(50) not null comment '角色英文名',
    name        varchar(50) not null comment '角色中文名',
    create_time datetime    not null on update CURRENT_TIMESTAMP comment '创建时间',
    update_time datetime    not null on update CURRENT_TIMESTAMP comment '修改时间',
    version     bigint         not null comment '乐观锁字段',
    deleted     tinyint         not null comment '逻辑删除字段'
);

create table user_role
(
    id          int auto_increment comment '用户角色表主键'
        primary key,
    user_id     varchar(50) not null comment '用户id',
    role_id     int         not null comment '角色id',
    create_time datetime    not null on update CURRENT_TIMESTAMP comment '创建时间',
    update_time datetime    not null on update CURRENT_TIMESTAMP comment '修改时间',
    version     bigint         not null comment '乐观锁字段',
    deleted     tinyint         not null comment '逻辑删除字段'
);

导入相关依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3.4</version>
</dependency>
<!-- 图片验证码 -->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_security?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver

    druid:
      aop-patterns: com.qingsongxyz.springsecurity #  配置Spring监控
      filters: 'stat,wall'
      stat-view-servlet:
        enabled: true # 打开监控统计功能
        login-username: admin
        login-password: admin
        reset-enable: true
      web-stat-filter:
        enabled: true # Web关联监控配置
      filter:
        stat:
          enabled: true # 开启sql监控
        wall:
          enabled: true # 开启防火墙
          db-type: mysql
          config:
            delete-allow: false
            drop-table-allow: false

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置日志

kaptcha图片验证码配置类:

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer kaptcha(){
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150"); //图片长度
        properties.setProperty("kaptcha.image.height", "50"); //图片高度
        //properties.setProperty("kaptcha.textproducer.char.string", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); //验证码值从此集合中获取(大小写字母)
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); //验证码值从此集合中获取(数字)
        properties.setProperty("kaptcha.textproducer.char.length", "4"); //验证码长度

        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
//返回base64验证码
@Slf4j
@RestController
public class VerifyCodeController {

    @Autowired
    private Producer producer;

    @GetMapping("/vc.png")
    public String getVerifyCode(HttpSession session) throws IOException {
        //1.生成验证码
        String code = producer.createText();
        //2.保存到session或redis
        log.info("VerifyCodeController code:{}", code);
        session.setAttribute("code", code);
        log.info("VerifyCodeController session code:{}", session.getAttribute("code"));
        //3.生成图片
        BufferedImage image = producer.createImage(code);
        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
        //将图片写入输出流中
        ImageIO.write(image, "png",fos);
        //4.返回base64编码
        return Base64.encodeBase64String(fos.toByteArray());
    }
}

编写实体类:

@Data
@Accessors(chain = true)
@TableName("role")
@ApiModel(value = "Role对象", description = "")
public class Role extends Model<Role> {

    @ApiModelProperty("角色id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty("角色英文名")
    @TableField("`role`")
    private String role;

    @ApiModelProperty("角色中文名")
    @TableField("`name`")
    private String name;

    @ApiModelProperty("创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @ApiModelProperty("修改时间")
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @ApiModelProperty("乐观锁字段")
    @TableField(value = "version", fill = FieldFill.INSERT)
    @Version
    private Long version;

    @ApiModelProperty("逻辑删除字段")
    @TableField(value = "deleted", fill = FieldFill.INSERT)
    @TableLogic
    private Integer deleted;


    @Override
    public Serializable pkVal() {
        return this.id;
    }

}
//User类实现UserDetails接口
@Data
@Accessors(chain = true)
@TableName("user")
@ApiModel(value = "User对象", description = "")
public class User extends Model<User> implements UserDetails {

    @ApiModelProperty("用户id")
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    @ApiModelProperty("用户名")
    @TableField("username")
    private String username;

    @ApiModelProperty("密码")
    @TableField("`password`")
    private String password;

    @ApiModelProperty("逻辑删除字段")
    @TableField(value = "deleted", fill = FieldFill.INSERT)
    @TableLogic
    private Integer deleted;

    @ApiModelProperty("用户是否过期")
    @TableField("account_expired")
    private Integer accountExpired;

    @ApiModelProperty("用户是否锁定")
    @TableField("account_locked")
    private Integer accountLocked;

    @ApiModelProperty("密码是否过期")
    @TableField("credentials_expired")
    private Integer credentialsExpired;

    @ApiModelProperty("创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @ApiModelProperty("修改时间")
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @ApiModelProperty("乐观锁字段")
    @TableField(value = "version", fill = FieldFill.INSERT)
    @Version
    private Long version;

    //关系属性 存储当前用户的角色信息
    @TableField(exist = false)
    private List<Role> roleList = new ArrayList<>();


    @Override
    public Serializable pkVal() {
        return this.id;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<SimpleGrantedAuthority> authorities = new HashSet<>();

        roleList.forEach(role -> {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRole());
            authorities.add(simpleGrantedAuthority);
        });
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountExpired == 0;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountLocked == 0;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsExpired == 0;
    }

    @Override
    public boolean isEnabled() {
        return deleted == 0;
    }
}

UserMapper:

@Mapper
public interface UserMapper extends BaseMapper<User> {

    List<Role> getRolesById(@RequestParam("userId") String userId);
}

UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qingsongxyz.springsecurity.mapper.UserMapper">
	<select id="getRolesById" resultType="com.qingsongxyz.springsecurity.pojo.Role">
        select
            r.id, r.role, r.name
        from
            role r
        left join
            user_role ur
        on
            r.id = ur.role_id
        where
            ur.user_id = #{userId}
    </select>
</mapper>

UserService:

public interface UserService extends IService<User> {

}

UserServiceImpl:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    
    public static final String ROLE_PREFIX = "ROLE_";

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

         QueryWrapper<User> userWrapper = new QueryWrapper<>();
        userWrapper.eq("username", username);
        User user = userMapper.selectOne(userWrapper);
        if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确");

        List<Role> roles = userMapper.getRolesById(user.getId());
        //给角色信息添加前缀ROLE_
        for (Role role : roles) {
            role.setRole(ROLE_PREFIX + role.getRole());
        }
        user.setRoleList(roles);
        return user;
    }
}

验证码异常类:

public class VerifyCodeException extends AuthenticationException {

    public VerifyCodeException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public VerifyCodeException(String msg) {
        super(msg);
    }
}

覆盖默认的UsernamePasswordAuthenticationFilter

@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    public static final String FORM_CODE_KEY = "code";

    private String codeParameter = FORM_CODE_KEY;

    public String getCodeParameter() {
        return codeParameter;
    }

    public void setCodeParameter(String codeParameter) {
        this.codeParameter = codeParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //判断是否为POST请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //判断是否json格式请求
        if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE))
        {
            try {
                //获取登录信息
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String code = userInfo.get(getCodeParameter());
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());

                //获取session中验证码
                String sessionCode = (String)request.getSession().getAttribute("code");

                log.info("code:{}, session code:{}", code, sessionCode);

                //验证码一致
                if(!ObjectUtils.isEmpty(code) && !ObjectUtils.isEmpty(sessionCode) && code.equals(sessionCode))
                {
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                    this.setDetails(request, authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
                throw new VerifyCodeException("验证码不一致!!!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        throw new InvalidParameterException("参数格式必须为json格式!!!");
    }
}

在配置类中给自定义的AuthenticationManager注入数据源:

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserServiceImpl userServiceImpl;

    //自定义的AuthenticationManager会覆盖SpringBoot自动配置的AuthenticationManager
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userServiceImpl);
    }

    //将自定义的AuthenticationManager暴露在容器中,使得在其他地方能够注入
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
   
    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        //配置登录认证请求
        loginFilter.setFilterProcessesUrl("/doLogin");
        //注入自定义的AuthenticationManager
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler(((request, response, authentication) > {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.OK); //200
            map.put("msg", "登录成功!");
            map.put("authentication", authentication);
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }));
        loginFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.UNAUTHORIZED); //401
            map.put("msg", "登录失败!!!");
            map.put("exception", exception.getMessage());
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }));
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/res/**").permitAll()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/vc.png").permitAll() //验证码放行
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                //未认证抛出异常,不显示默认登录页面
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) ->
{                  				response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);                 response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
response.getWriter().println("请先进行认证!");
response.flushBuffer();
})
                .and()
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
                .logoutSuccessHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
                .csrf().disable();
     
        //使用自定义的loginFilter覆盖默认的UsernamePasswordAuthenticationFilter
        http.addFilterAt(loginFilter(), 							UsernamePasswordAuthenticationFilter.class);
    }
}

数据库中有三个用户:

用户名密码权限
tom12345customer
rootrootadmin
adminadminadmin/super

测试:

访问验证码接口获取base64编码
在这里插入图片描述

通过在线网站转换成图片:https://tool.chinaz.com/tools/imgtobase

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

六、密码加密

​ 前面我们已经知道真正地认证过程由AbstractUserDetailsAuthenticationProvider

authenticate方法完成,在该方法中先是调用retrieveUser方法通过username加载用户信息,

然后调用preAuthenticationChecks.check方法检查用户是否启用、是否过期、是否锁定,

调用**additionalAuthenticationChecks**方法检查用户密码是否正确。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

因此,我们只需要在存储用户密码时,存入{加密方式}对应加密方法后的密文即可,代理类会自动识别不同加

密方式进行匹配

在这里插入图片描述

推荐使用BCrypt加密:

public class BCryptPasswordEncoder implements PasswordEncoder {
    private Pattern BCRYPT_PATTERN;
    private final Log logger;
    private final int strength;
    private final BCryptPasswordEncoder.BCryptVersion version;
    private final SecureRandom random;
    
    //strength >= 4 && strength <= 31
    
    public BCryptPasswordEncoder() {
        this(-1);
        /*
        BCryptPasswordEncoder(-1);
        BCryptPasswordEncoder(-1, (SecureRandom)null);
        BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A, -1, 								(SecureRandom)null);
        ------------------------------------------------------------------
         this.version = BCryptPasswordEncoder.BCryptVersion.$2A;
         this.strength = 10;
         this.random = null;
        */
    }

    public BCryptPasswordEncoder(int strength) {
        this(strength, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version) {
        this(version, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, SecureRandom random) {
        this(version, -1, random);
    }

    public BCryptPasswordEncoder(int strength, SecureRandom random) {
        this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength) {
        this(version, strength, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
        this.logger = LogFactory.getLog(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.version = version;
            this.strength = strength == -1 ? 10 : strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }
    public String encode(CharSequence rawPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else {
            String salt = this.getSalt();
            return BCrypt.hashpw(rawPassword.toString(), salt);
        }
    }

 	...

    //BCrypt版本号
    public static enum BCryptVersion {
        $2A("$2a"),
        $2Y("$2y"),
        $2B("$2b");

        private final String version;

        private BCryptVersion(String version) {
            this.version = version;
        }

        public String getVersion() {
            return this.version;
        }
    }
}
//加密后的密码长度为60(所以设计数据库时得保证该字段长度不小于60)
$2a$10$QwS9HMTOeDGRsezR7pT.jOD2xUYKgBpprCKzrzcymxj5zNkc4HTJO
$2y$10$ubOewGxo618uwZ8MugxyC.y/i5SxjZk0QwkgoBGNQy6L8W.uROVim
$2b$10$bA0f7n.9uvFbKQanB2nq.u8Y84c2CUVGqGVKYFzXZpRuQ7ONZtzY2

默认使用$2a版本,循环加盐10次(最少4次,最多31次)

如果只使用一种加密方式并不会改变时,可以直接向容器中注入该类型的PasswordEncoder
在这里插入图片描述

在这里插入图片描述

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

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

注意:

固定死加密方式后,密码不需要添加{加密方式},直接使用加密后的密文,但是无法更新加密方式不灵活

密码自动升级:

更改加密方式重新进行登录后,将数据库中存储的原本加密方式加密的密码更新为新的加密方式加密的密码

在这里插入图片描述

//实现UserDetailsPasswordService接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService, UserDetailsPasswordService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<User> userWrapper = new QueryWrapper<>();
        userWrapper.eq("username", username);
        User user = userMapper.selectOne(userWrapper);
        if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确");

        List<Role> roles = userMapper.getRolesById(user.getId());
        user.setRoleList(roles);

        return user;
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        //根据用户名更新密码
        UpdateWrapper<User> wrapper = new UpdateWrapper<>();
        wrapper.eq("username", user.getUsername());
        wrapper.set("password", newPassword);
        int update = userMapper.update((User) user, wrapper);
        return user;
    }
}

测试明文存储密码自动升级为Bcrypt加密的密码:

在这里插入图片描述

在这里插入图片描述

七、Remember me

1.源码分析

登录记住我,认证成功后服务器生成cookie向前端返回:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

//成员变量
private String parameter = "remember-me";

protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
    if (this.alwaysRemember) { //如果总是开启rememberMe的话直接返回true
        return true;
    } else {
        //获取请求参数remember-me的值 只有是"true"、"on"、"yes"、"1"时才返回true
        String paramValue = request.getParameter(parameter); 
        if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
            this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
            return false;
        } else {
            return true;
        }
    }
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

session30分钟过期登录信息失效后,携带名为remember-me的cookie在服务器自动认证:

在这里插入图片描述

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    //如果用户认证过session没过期直接放行 否则尝试自动登录
    if (SecurityContextHolder.getContext().getAuthentication() != null) {
        this.logger.debug(LogMessage.of(() -> {
            return "SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'";
        }));
        chain.doFilter(request, response);
    } else {
        //调用AbstractRememberMeServices类的autoLogin方法自动登录
        Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
        if (rememberMeAuth != null) {
            try {
                //认证 设置登录用户信息
                rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                context.setAuthentication(rememberMeAuth);
                SecurityContextHolder.setContext(context);
                this.onSuccessfulAuthentication(request, response, rememberMeAuth);
                this.logger.debug(LogMessage.of(() -> {
                    return "SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'";
                }));
                if (this.eventPublisher != null) {
                    this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
                }

                if (this.successHandler != null) {
                    this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
                    return;
                }
            } catch (AuthenticationException var6) {
                this.logger.debug(LogMessage.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '%s'; invalidating remember-me token", rememberMeAuth), var6);
                this.rememberMeServices.loginFail(request, response);
                this.onUnsuccessfulAuthentication(request, response, var6);
            }
        }

        chain.doFilter(request, response);
    }
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

默认使用TokenBasedRememberMeServices生成的cookie由用户名、过期时间、签名三部分组成,这个cookie

除非过期否则是不改变的,因此我们只需要携带此cookie在其他地方也能认证成功,安全性较低

我们可以使用另外一个实现类PersistentTokenBasedRememberMeServices

//series长度
private int seriesLength = 16;
//token长度
private int tokenLength = 16;

//得到随机16位进行base64加密后的series字符串
protected String generateSeriesData() {
    byte[] newSeries = new byte[this.seriesLength];
    this.random.nextBytes(newSeries);
    return new String(Base64.getEncoder().encode(newSeries));
}

//得到随机16位进行base64加密后的token字符串
protected String generateTokenData() {
    byte[] newToken = new byte[this.tokenLength];
    this.random.nextBytes(newToken);
    return new String(Base64.getEncoder().encode(newToken));
}

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
    String username = successfulAuthentication.getName();
    this.logger.debug(LogMessage.format("Creating new persistent login for user %s", username));
    //将用户名、series、token、当前时间封装成一个PersistentRememberMeToken对象
    PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

    try {
        //创建一个token
        this.tokenRepository.createNewToken(persistentToken);
        //向前端返回cookie
        this.addCookie(persistentToken, request, response);
    } catch (Exception var7) {
        this.logger.error("Failed to save persistent token ", var7);
    }
}

在这里插入图片描述

在这里插入图片描述

session过期,自动登录过程验证remember-mecookie并更新该cookie:

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
    if (cookieTokens.length != 2) { 
        throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
    } else {
        String presentedSeries = cookieTokens[0]; //获取series
        String presentedToken = cookieTokens[1]; //获取token
        //通过series获取封装的PersistentRememberMeToken信息
        PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
        //如果token为null或者两个token不一致或者cookie超时过期都会抛出异常
        if (token == null) {
            throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
        } else if (!presentedToken.equals(token.getTokenValue())) {
            this.tokenRepository.removeUserTokens(token.getUsername());
            throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
            throw new RememberMeAuthenticationException("Remember-me login has expired");
        } else {
            this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries()));
              	
            //如果两个token相同,则将原来的series和一个新的token生成一个新的cookie返回给前端,并更新tokenRepository中的值
            PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

            try {
                this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                this.addCookie(newToken, request, response);
            } catch (Exception var9) {
                this.logger.error("Failed to update token: ", var9);
                throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
            }
		
            //返回认证信息
            return this.getUserDetailsService().loadUserByUsername(token.getUsername());
        }
    }
}

PersistentTokenBasedRememberMeServices除了能更新cookie值外,还支持数据库存储,避免服务器宕机后

记住我失效

//成员变量 默认将PersistentRememberMeToken对象保存在内存中
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.配置记住我

在这里插入图片描述

修改LoginFilter类:

@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    public static final String FORM_CODE_KEY = "code";

    public static final String FORM_REMEMBER_ME_KEY = "remember-me";

    private String codeParameter = FORM_CODE_KEY;

    private String rememberMeParameter = FORM_REMEMBER_ME_KEY;

    public String getCodeParameter() {
        return codeParameter;
    }

    public String getRememberMeParameter() {
        return rememberMeParameter;
    }

    public void setRememberMeParameter(String rememberMeParameter) {
        this.rememberMeParameter = rememberMeParameter;
    }

    public void setCodeParameter(String codeParameter) {
        this.codeParameter = codeParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //判断是否为POST请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //判断是否json格式请求
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                //获取登录信息
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String code = userInfo.get(getCodeParameter());
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                String rememberMe = userInfo.get(getRememberMeParameter());

                //获取session中验证码
                String sessionCode = (String) request.getSession().getAttribute("code");

                log.info("code:{}, session code:{}", code, sessionCode);

                //验证码一致
                if (!ObjectUtils.isEmpty(code) && !ObjectUtils.isEmpty(sessionCode) && code.equals(sessionCode)) {

                    //如果记住我 存入request中方便后续处理
                    if (!ObjectUtils.isEmpty(rememberMe)) {
                        request.setAttribute(getRememberMeParameter(), rememberMe);
                    }

                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                    this.setDetails(request, authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
                throw new VerifyCodeException("验证码不一致!!!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        throw new InvalidParameterException("参数格式必须为json格式!!!");
    }
}
/**
 * 前后端分离自定义接受RememberMe参数
 */
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {

    public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }

    //重写rememberMeRequested方法
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        //由于在认证成功后才会生成cookie,loginFilter先获取了inputStream,此处无法重复获取,才通过request域获取rememberMe参数
        String paramValue = request.getAttribute(parameter) == null ? null : request.getAttribute(parameter).toString();
        if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
            return false;
        } else {
            return true;
        }
    }
}

Security配置类:

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserServiceImpl userServiceImpl;

    @Autowired
    private DataSource dataSource;

    //注入JdbcTokenRepositoryImpl 进行cookie持久化
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(false); //第一次启动需要设置为true 自动创建persistent_logins表 后续设置为false
        return jdbcTokenRepository;
    }

    //注入自定义的RememberMeServices
    @Bean
    public RememberMeServices RememberMeServices(){
        //key为UUID 数据源为mysql 持久化cookie
        return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userServiceImpl, persistentTokenRepository());
    }

    //自定义的AuthenticationManager会覆盖SpringBoot自动配置的AuthenticationManager
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userServiceImpl);
    }

    //将自定义的AuthenticationManager暴露在容器中,使得在其他地方能够注入
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/doLogin");
        //配置自定义RememberMeServices生成cookie
        loginFilter.setRememberMeServices(RememberMeServices());
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.OK.value()); //200
            map.put("msg", "登录成功!");
            map.put("authentication", authentication);
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }));
        loginFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.UNAUTHORIZED.value()); //401
            map.put("msg", "登录失败!!!");
            map.put("exception", exception.getMessage());
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }));
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/res/**").permitAll()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/vc.png").permitAll()
                .anyRequest().authenticated()
                .and()
                .rememberMe()
                //配置使用自定义RememberMeServices验证cookie参数
                .rememberMeServices(RememberMeServices()) 
                .and()
                .formLogin()
                .and()
                //未认证抛出异常,不显示默认登录页面
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
                    response.getWriter().println("请先进行认证!");
                    response.flushBuffer();
                })
                .and()
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
                .logoutSuccessHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
                .csrf().disable();

        //覆盖UsernamePasswordAuthenticationFilter
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

测试:

访问验证码接口:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

八、会话管理(session)

1.并发session管理

在这里插入图片描述

成员变量,默认值为NullAuthenticatedSessionStrategy

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

会话认证策略解释
ChangeSessionIdAuthenticationStrategy仍使用原来的session,只修改下session id
SessionFixationProtectionStrategy创建一个新的session,将原来session的属性迁移到新的session中
CompositeSessionAuthenticationStrategy组合多个SessionAuthenticationStrategy
ConcurrentSessionControlAuthenticationStrategy限制同一用户同时登陆的次数
CsrfAuthenticationStrategy登陆成功之后,更换原来的csrf token
NullAuthenticatedSessionStrategy空实现
RegisterSessionAuthenticationStrategy注册新session信息到SessionRegistry

设置同一用户会话最大并发数maximumSessions和会话过期回调expiredSessionStrategy

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
          	.anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout().logoutRequestMatcher(new OrRequestMatcher(
                    new AntPathRequestMatcher("/logout", "GET"),
                    new AntPathRequestMatcher("/logout", "POST")
            ))
            .logoutSuccessHandler(((request, response, authentication) -> {
                Map<String, Object> map = new HashMap<>();
                map.put("code", 200);
                map.put("msg", "注销成功!!!");
                map.put("authentication", authentication);
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(map);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
       		    response.flushBuffer();
            }))
            .and()
            .csrf().disable()
            .sessionManagement() //会话管理
            .maximumSessions(1) //允许会话最大并发只能一个客户端
            .expiredSessionStrategy(event -> { //设置用户被挤下线(会话过期)回调
                HttpServletResponse response = event.getResponse();
                Map<String, Object> map = new HashMap<>();
                map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value()); //500
                map.put("msg", "当前会话已失效,请重新登录!!!");
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(map);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
                response.flushBuffer();
            });
}

同一用户在不同浏览器登录(挤下线):
在这里插入图片描述

在这里插入图片描述

设置被挤下线后不能再次登录maxSessionsPreventsLogin

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
          	.anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout().logoutRequestMatcher(new OrRequestMatcher(
                    new AntPathRequestMatcher("/logout", "GET"),
                    new AntPathRequestMatcher("/logout", "POST")
            ))
            .logoutSuccessHandler(((request, response, authentication) -> {
                Map<String, Object> map = new HashMap<>();
                map.put("code", 200);
                map.put("msg", "注销成功!!!");
                map.put("authentication", authentication);
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(map);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
                response.flushBuffer();
            }))
            .and()
            .csrf().disable()
            .sessionManagement() //会话管理
            .maximumSessions(1) //允许会话最大并发只能一个客户端
            .expiredSessionStrategy(event -> { //设置用户被挤下线(会话过期)回调
                HttpServletResponse response = event.getResponse();
                Map<String, Object> map = new HashMap<>();
                map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value()); //500
                map.put("msg", "当前会话已失效,请重新登录!!!");
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(map);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
                response.flushBuffer();
            })
            .maxSessionsPreventsLogin(true); //不允许再次登录
}

同一用户在不同浏览器登录(不允许再次登录):

在这里插入图片描述

在这里插入图片描述

2.集群下session共享

导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--swagger-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

配置文件:

spring:
  redis:
   host: xxx
   port: 6379
   #password: xxx # 如果有密码需要配置
   client-type: lettuce
  #解决SpringBoot2.6以上版本和Swagger3的冲突问题
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

Redis配置类:

@Configuration
public class RedisConfig {

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

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        //方法已过时
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);

        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);

        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }
}

Swagger配置类:

@Configuration
public class SwaggerConfig {

    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.OAS_30)
                .groupName("开发一组")
                .select()
                //.paths(PathSelectors.ant("/list/**")) //访问路径过滤
            .apis(RequestHandlerSelectors.basePackage("com.qingsongxyz.springsecurity.controller")) //包过滤
                .build()
                .apiInfo(createApiInfo())
                .enable(true);
    }

    @Bean
    public ApiInfo createApiInfo() {
        return new ApiInfo("qingsongxyz Swagger",
                "qingsongxyz Api Documentation",
                "3.0",
                "http:xxx",
                new Contact("qingsongxyz", "http:xxx", "xxx@qq.com"),
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList());
    }

}

登录控制类(用于测试):

@Controller
public class LoginController {

    @PostMapping("/doLogin")
    public void login(@RequestBody Map<String, String> map) {

    }
}

SpringSecurity核心配置类:

@Resource
private FindByIndexNameSessionRepository repository;

//注入sessionRegistry
@Bean
public SessionRegistry sessionRegistry(){
    return new SpringSessionBackedSessionRegistry<>(repository);
}

//设置SessionAuthenticationStrategy为ConcurrentSessionControlAuthenticationStrategy 控制并发session
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy(){
    ConcurrentSessionControlAuthenticationStrategy strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
    strategy.setMaximumSessions(1); //同一账户只创建一次session
    //strategy.setExceptionIfMaximumExceeded(true); //不允许重复登录
    return strategy;
}

@Bean
public LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    loginFilter.setFilterProcessesUrl("/doLogin");
    loginFilter.setRememberMeServices(RememberMeServices());
    //配置SessionAuthenticationStrategy
    loginFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    loginFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
        Map<String, Object> map = new HashMap<>();
        map.put("code", HttpStatus.OK.value()); //200
        map.put("msg", "登录成功!");
        map.put("authentication", authentication);
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        response.flushBuffer();
    }));
    loginFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
        Map<String, Object> map = new HashMap<>();
        map.put("code", HttpStatus.UNAUTHORIZED.value()); //401
        map.put("msg", "登录失败!!!");
        map.put("exception", exception.getMessage());
        //当产生的异常是SessionAuthenticationException时
        if(exception instanceof SessionAuthenticationException)
        {
            map.put("exception", "该用户已经从处于登录状态!!!");
        }
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        response.flushBuffer();
    }));
    return loginFilter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .mvcMatchers("/res/**").permitAll()
        .mvcMatchers("/login").permitAll()
        .mvcMatchers("/swagger-resources/**","/swagger-ui/**", "/v3/**", "/error").permitAll() //放行swagger相关资源
        .mvcMatchers("/vc.png").permitAll()
        .anyRequest().authenticated()
        .and()
        .rememberMe()
        .rememberMeServices(RememberMeServices()) 
        .and()
        .formLogin()
        .and()
        //未认证抛出异常,不显示默认登录页面
        .exceptionHandling()
        .authenticationEntryPoint((request, response, authException) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
            response.getWriter().println("请先进行认证!");
        })
        .and()
        .logout(logout -> logout.deleteCookies("JSESSIONID"))
        .logout().logoutRequestMatcher(new OrRequestMatcher(
        new AntPathRequestMatcher("/logout", "GET"),
        new AntPathRequestMatcher("/logout", "POST")
    ))
        .logoutSuccessHandler(((request, response, authentication) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", 200);
            map.put("msg", "注销成功!!!");
            map.put("authentication", authentication);
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }))
        .and()
        .csrf().disable()
        .sessionManagement() //会话管理
        .maximumSessions(1) //同一账户只创建一次session
        .expiredSessionStrategy(event -> { //用户被挤下线处理
            HttpServletResponse response = event.getResponse();
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value()); //500
            map.put("msg", "当前会话已失效,请重新登录!!!");
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        })
        .sessionRegistry(sessionRegistry());

    //覆盖UsernamePasswordAuthenticationFilter
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

测试两个项目运行在不同端口登录同一用户:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

使用Postman访问8080登录:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

使用Swagger访问8081登录:

在这里插入图片描述

设置可以挤下线输出提示信息:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

设置setExceptionIfMaximumExceeded不允许同一用户重复登录:

在这里插入图片描述

session存储到redis中默认过期时间为30分钟(1800s):

在这里插入图片描述

spring:session是默认的Redis HttpSession的前缀,每一个session都会创建3组数据:

  • hash结构,spring:session:sessions存储主要内容

在这里插入图片描述

  • String结构,spring:session:expires用于ttl过期时间

在这里插入图片描述

  • set结构,spring:session:expirations过期时间记录,由于redis清除过期key的行为是一个异步行为且是一个低优先级的行为,可能会导致session不被清除,此项负责session的清除

在这里插入图片描述

在这里插入图片描述

spring.session.timeoutserver.servlet.session.timeout都无法设置redis中session的过期时间,可以

通过@EnableRedisHttpSession进行配置
在这里插入图片描述

@EnableOpenApi
@MapperScan("com.qingsongxyz.springsecurity.mapper")
@SpringBootApplication
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 300)
public class SpringSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }
}

在这里插入图片描述

九、跨域请求访问CSRF

跨站请求伪造(Cross-site request forgery),也被称为 one-click attack 或者 **session **

riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非

本意操作的攻击方法。

XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

在这里插入图片描述

关闭Spring Security中Csrf防御,模拟Csrf攻击:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout().logoutRequestMatcher(new OrRequestMatcher(
                    new AntPathRequestMatcher("/logout", "GET"),
                    new AntPathRequestMatcher("/logout", "POST")
            ))
            .logoutSuccessHandler(((request, response, authentication) -> {
                Map<String, Object> map = new HashMap<>();
                map.put("code", 200);
                map.put("msg", "注销成功!!!");
                map.put("authentication", authentication);
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(map);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
                response.flushBuffer();
            }))
            .and()
            .csrf().disable(); //关闭csrf防御
}

转账handler:

@GetMapping("/withdraw")
public String withdraw(String name, Double money) {
    return "成功向" + name + "转账" + money + "元...";
}

另一个页签,html中包含Csrf攻击:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSRF</title>
</head>
<body>
    <!--请求域名需要一致-->
    <form action="http://localhost:8080/withdraw" method="get">
        <input hidden type="text" name="name" value="李四">
        <input hidden type="text" name="money" value="10000">
        <button>点击有惊喜!</button>
    </form>
</body>
</html>

默认登录页面源代码:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

开启Spring Security中Csrf防御(前后端分离):

服务器生成一个csrf token,每次响应请求时对该token进行验证

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .and()
            //未认证抛出异常,不显示默认登录页面
            .exceptionHandling()
            .authenticationEntryPoint((request, response, authException) -> {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
                response.getWriter().println("请先进行认证!");
            })
            .logout().logoutRequestMatcher(new OrRequestMatcher(
                    new AntPathRequestMatcher("/logout", "GET"),
                    new AntPathRequestMatcher("/logout", "POST")
            ))
            .logoutSuccessHandler(((request, response, authentication) -> {
                Map<String, Object> map = new HashMap<>();
                map.put("code", 200);
                map.put("msg", "注销成功!!!");
                map.put("authentication", authentication);
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(map);

                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
            }))
            .and()
            .csrf() //开启csrf防御
        	//将令牌保存到cookie中,允许前端获取
		   .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}

在这里插入图片描述

CsrfFilter类:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

十、跨域配置CORS

CORS(Cross-Orgin Resource Sharing),同源 = 协议 + 主机 + 端口号(都相同)

新增一组Http请求头,除GET以外的请求浏览器必须先发送OPTIONS方式请求(预检请求prenightst),查看

服务器是否支持即将发送的跨域请求

请求头说明
Access-Control-Allow-Origin可以访问的域
Access-Control-Request-Methods发起跨域请求所使用的方法
Access-Control-Max-Age预检请求有效期,在预检之后的这段时间内不会再次发送预检请求
1.Spring跨域方式

新建子模块TestCors用于测试跨域

导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

控制器:

@RestController
public class TestController {

    @GetMapping("/test")
    public String test(){
        return "success...";
    }
}

前端页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>跨域请求</title>
    <script src="axios.js"></script>
</head>
<body>
    <h1>跨域请求测试</h1>
    <h1 class="content"></h1>
    <script>
        var content = document.getElementsByClassName("content")[0];

        axios.get(`http://localhost:8080/test`).then(
            response => {
                content.textContent = response.data
            },
            error => {
                content.textContent = error.message
            }
        )
    </script>
</body>
</html>

启动访问:

在这里插入图片描述

第一种解决方案:

在控制器添加@CrossOrigin注解

@RestController
@CrossOrigin //设置允许跨域
public class TestController {

    @GetMapping("/test")
    public String test(){
        return "success...";
    }
}

在这里插入图片描述

第二种解决方案(全局允许跨域):

@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") //所有请求允许跨域
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(false)
                .exposedHeaders("")
                .maxAge(1800L); //设置预检请求时间为30分钟
    }
}

在这里插入图片描述

第三种解决方案(全局允许跨域):

@Configuration
public class MyCorsFilter {

    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean(){
        FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(1800L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(-1);
        return registrationBean;
    }
}

在这里插入图片描述

2.Spring Security跨域

添加security依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

启动测试Spring跨域方式失效,前两种请求被Spring Security拦截需要进行认证,第三种注册Filter需要在

Seucity FilterChain中的所有Filter之前执行才能成功

在这里插入图片描述

解决方法:

security配置类:

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    //用户信息存储在内存中
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("root").password("{noop}root").roles("admin", "super").build());
        return userDetailsService;
    }

    //跨域配置
    public CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(1800L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
                    response.getWriter().println("请先进行认证!");
                    response.flushBuffer();
                })
                .and()
                .logout(logout -> logout.deleteCookies("JSESSIONID"))
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
            	.logoutSuccessHandler(((request, response, authentication) -> 						   {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
                .cors()
                .configurationSource(corsConfigurationSource()) //指定跨域
                .and()
                .csrf().disable();
    }
}

测试:

在这里插入图片描述

十、Security异常处理

认证异常(AuthenticationException):

在这里插入图片描述

授权异常(AccessDeniedException):

在这里插入图片描述

Security配置类:

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    //用户信息存储在内存中
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("root").password("{noop}root").roles("admin", "super").build());
        userDetailsService.createUser(User.withUsername("customer").password("{noop}123456").roles("customer").build());
        return userDetailsService;
    }

    public CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(1800L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/test").hasRole("admin")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
                    response.getWriter().println("请先进行认证!");
                    response.flushBuffer();
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.FORBIDDEN.value()); //403
                    response.getWriter().println("没有权限访问!!!");
                    response.flushBuffer();
                })
                .and()
                .logout(logout -> logout.deleteCookies("JSESSIONID"))
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
                .logoutSuccessHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
                .cors()
                .configurationSource(corsConfigurationSource()) //指定跨域
                .and()
                .csrf().disable();
    }
}

启动测试:

customer登录访问/test

在这里插入图片描述

在这里插入图片描述

root登录访问/test

在这里插入图片描述

在这里插入图片描述

十一、授权

权限管理方式(RBAC):

基于角色的权限管理(Role Base Access Control):用户 - 角色 - 资源

基于资源的权限管理(Resource Base Access Control):用户 - 权限 - 资源

Spring Security中的权限管理策略:

  • 基于URL权限管理(FilterSecurityInterceptor)
    • 通过过滤器实现,拦截HTTP请求,根据地址进行权限校验
  • 基于方法权限管理(MethodSecurityInterceptor)
    • 当调用方法时,通过AOP将操作拦截下来,进行权限校验
1.URL权限管理

控制器:

@RestController
@CrossOrigin //设置跨域
public class TestController {

    @GetMapping("/test")
    public String test(){
        return "success...";
    }

    @GetMapping("/super")
    public String super_(){
        return "super homePage...";
    }

    @GetMapping("/admin")
    public String admin(){
        return "admin homePage...";
    }

    @GetMapping("/customer")
    public String customer(){
        return "customer homePage...";
    }
    
    @GetMapping("/info")
    public String info(){
        return "info content...";
    }
}

Security配置类:

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    //用户信息存储在内存中
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("root").password("{noop}root").roles("admin", "super").build()); //创建root用户具有admin、super角色
        userDetailsService.createUser(User.withUsername("customer").password("{noop}customer").roles("customer").build()); //创建customer用户具有customer角色
        userDetailsService.createUser(User.withUsername("zhangsan").password("{noop}123456").authorities("read_info_content").build()); //创建zhangsan用户具有read_info_content权限
        return userDetailsService;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/super").hasRole("super") //访问super接口需要super角色
                .mvcMatchers("/admin").hasRole("admin") //访问admin接口需要admin角色
                .mvcMatchers("/customer").hasRole("customer") //访问customer接口需要customer角色
                .mvcMatchers("/info").hasAuthority("read_info_content") //访问info接口需要read_info_content权限
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/test") //认证成功跳转到/test路径
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
                    response.getWriter().println("请先进行认证!");
                    response.flushBuffer();
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.FORBIDDEN.value()); //403
                    response.getWriter().println("没有权限访问!!!");
                    response.flushBuffer();
                })
                .and()
                .logout(logout -> logout.deleteCookies("JSESSIONID"))
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
                .logoutSuccessHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
                .csrf().disable();
    }
}

启动测试:

root用户登录:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

customer用户登录:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

zhangsan用户登录:

在这里插入图片描述

在这里插入图片描述

权限表达式(SecurityExpressionOperations接口):

在这里插入图片描述

方法说明
hasAuthority(String authority)是否具有指定权限
hasAnyAuthority(String… authorities)是否具有指定数组中任意一个权限
hasRole(String role)是否具有指定角色
hasAnyRole(String… roles)是否具有指定数组中任意一个角色
permitAll()放行所有请求
denyAll()拒绝所有请求
isAnonymous()是否是一个匿名用户
isAuthenticated()是否认证成功
isRememberMe()是否通过记住我自动登录
isFullyAuthenticated()是否既不是匿名用户也不是通过记住我自动登录
hasPermission(Object target, Object permission)是否具有指定目标的指定权限
hasPermission(Object targetId, String targetType, Object permission)是否具有指定目标的指定权限

AbstractRequestMatcherRegistry类中:

antMatchers只会匹配指定的路径

mvcMatchers:

在这里插入图片描述

regexMatchers:

在这里插入图片描述

2.方法权限管理

URL权限管理通过FilterSecurityInterceptor过滤器只能在请求前进行处理,而方法权限管理通过AOP可以在

请求前后都进行处理

@EnableGlobalMethodSecurity:开启权限注解进行方法权限管理

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {

   //是否开启Spring Security提供的权限注解(默认不开启)
   boolean prePostEnabled() default false;

   //是否开启Spring Security提供的@Secured(默认不开启)
   boolean securedEnabled() default false;

   //是否开启JSR-250提供的注解(默认不开启)
   boolean jsr250Enabled() default false;

   boolean proxyTargetClass() default false;

   AdviceMode mode() default AdviceMode.PROXY;

   int order() default Ordered.LOWEST_PRECEDENCE;

}

权限注解:

注解说明来源是否支持权限表达式
@PreAuthorize在目标方法执行前进行权限校验Spring Security
@PostAuthorize在目标方法执行后进行权限校验Spring Security
@PreFilter在目标方法执行前对方法参数进行过滤Spring Security
@PostFilter在目标方法执行后对返回结果进行过滤Spring Security
@Secured访问目标方法具有相应的角色Spring Security
@DenyAll拒绝所有访问JSR250
@PermitAll允许所有访问JSR250
@RolesAllowed访问目标方法具有相应的角色JSR250

修改Security配置类开启权限注解:

@Configuration
//开启所有权限注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
	......
}

控制器:

@RestController
public class RoleController {

    @PreAuthorize("hasRole('customer')")
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    //登录的用户名称是否和参数一致
    @PreAuthorize("authentication.name==#name")
    @GetMapping("/name")
    public String hello(String name){
        return "hello " + name;
    }

    //filterObject为参数people   		
    @PreFilter(value = "filterObject.gender.equals('男')", filterTarget = "people")
    @PostMapping("/people")
    public void addPeople(@RequestBody List<Person> people){
        System.out.println(people);
    }

    @PostAuthorize(value = "returnObject.age==18") //returnObject为返回值 年龄是否为18
    @GetMapping("/person")
    public Person getPerson(Integer age){
        return new Person("张三", age, "男");
    }

    @PostFilter(value = "filterObject.age%2==0") //filterObject为返回值    		
    @GetMapping("/list/people")
    public List<Person> getPeople(){
        ArrayList<Person> people = new ArrayList<>();
        for (int i = 1; i < 5; i++) {
            people.add(new Person("王" + i, i, "男"));
        }
        return people;
    }

    @Secured({"ROLE_admin", "ROLE_customer"}) //具有任意一个角色即可访问
    @GetMapping("/secured")
    public String secured(){
        return "secured...";
    }

    @PermitAll
    @GetMapping("/permitAll")
    public String permitAll(){
        return "permitAll...";
    }

    @DenyAll
    @GetMapping("/denyAll")
    public String denyAll(){
        return "denyAll...";
    }

    @RolesAllowed({"ROLE_admin", "ROLE_customer"}) //具有任意一个角色即可访问
    @GetMapping("/rolesAllowed")
    public String rolesAllowed(){
        return "rolesAllowed...";
    }
}

customer用户登录:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.原理分析

URL权限管理分析:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.基于角色的权限管理

数据库创建资源路径表和角色路径表:

CREATE TABLE "pattern" (
  "id" int NOT NULL AUTO_INCREMENT COMMENT '资源路径id',
  "pattern" varchar(255) NOT NULL COMMENT '资源路径',
  "create_time" datetime NOT NULL COMMENT '创建时间',
  "update_time" datetime NOT NULL COMMENT '修改时间',
  "version" bigint NOT NULL DEFAULT '0' COMMENT '乐观锁字段',
  "deleted" tinyint NOT NULL COMMENT '逻辑删除字段',
  PRIMARY KEY ("id")
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE "role_pattern" (
  "id" int NOT NULL AUTO_INCREMENT COMMENT '角色路径表id',
  "pattern_id" int NOT NULL COMMENT '资源路径id',
  "role_id" int NOT NULL COMMENT '角色id',
  "create_time" datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  "update_time" datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  "version" bigint NOT NULL DEFAULT '0' COMMENT '乐观锁字段',
  "deleted" tinyint NOT NULL COMMENT '逻辑删除字段',
  PRIMARY KEY ("id")
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

插入数据:

@SpringBootTest
class SpringSecurityApplicationTests {

    @Autowired
    private PatternMapper patternMapper;

    @Autowired
    private RolePatternMapper rolePatternMapper;

    @Test
    void addPattern(){
        Pattern pattern = new Pattern();
        pattern.setPattern("/super/**");

        Pattern pattern1 = new Pattern();
        pattern1.setPattern("/admin/**");

        Pattern pattern2 = new Pattern();
        pattern2.setPattern("/customer/**");

        patternMapper.insert(pattern);
        patternMapper.insert(pattern1);
        patternMapper.insert(pattern2);
    }

    @Test
    void addRolePattern(){
        RolePattern rolePattern = new RolePattern();
        rolePattern.setRoleId(3);
        rolePattern.setPatternId(1);

        RolePattern rolePattern1 = new RolePattern();
        rolePattern1.setRoleId(2);
        rolePattern1.setPatternId(2);

        RolePattern rolePattern2 = new RolePattern();
        rolePattern2.setRoleId(1);
        rolePattern2.setPatternId(3);

        rolePatternMapper.insert(rolePattern);
        rolePatternMapper.insert(rolePattern1);
        rolePatternMapper.insert(rolePattern2);
    }
}

数据库信息:

用户名密码权限
tom12345customer
rootrootadmin
adminadminadmin/super
资源路径权限
/super/**super
/admin/**admin
/customer/**customer

实体类:

@Getter
@Setter
@Accessors(chain = true)
@TableName("pattern")
@ApiModel(value = "Pattern对象", description = "")
public class Pattern extends Model<Pattern> {

    @ApiModelProperty("资源路径id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty("资源路径")
    @TableField("pattern")
    private String pattern;

    @ApiModelProperty("创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @ApiModelProperty("修改时间")
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @ApiModelProperty("乐观锁字段")
    @TableField(value = "version", fill = FieldFill.INSERT)
    @Version
    private Long version;

    @ApiModelProperty("逻辑删除字段")
    @TableField(value = "deleted", fill = FieldFill.INSERT)
    @TableLogic
    private Integer deleted;

    //关系属性 资源路径所需的角色信息
    @TableField(exist = false)
    private List<Role> roleList;

    @Override
    public Serializable pkVal() {
        return this.id;
    }
}
@Getter
@Setter
@Accessors(chain = true)
@TableName("role_pattern")
@ApiModel(value = "RolePattern对象", description = "")
public class RolePattern extends Model<RolePattern> {

    @ApiModelProperty("角色路径表id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty("资源路径id")
    @TableField("pattern_id")
    private Integer patternId;

    @ApiModelProperty("角色id")
    @TableField("role_id")
    private Integer roleId;

    @ApiModelProperty("创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @ApiModelProperty("修改时间")
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @ApiModelProperty("乐观锁字段")
    @TableField(value = "version", fill = FieldFill.INSERT)
    @Version
    private Long version;

    @ApiModelProperty("逻辑删除字段")
    @TableField(value = "deleted", fill = FieldFill.INSERT)
    @TableLogic
    private Integer deleted;


    @Override
    public Serializable pkVal() {
        return this.id;
    }
}

PatternMapper:

@Mapper
public interface PatternMapper extends BaseMapper<Pattern> {

    List<Pattern> getPatternList();
}

PatternMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qingsongxyz.springsecurity.mapper.PatternMapper">
    <select id="getPatternList" resultMap="BaseResultMap">
        select
            p.id, p.pattern, r.role, r.name
        from pattern p
        left join
            role_pattern rp
        on
            rp.pattern_id = p.id
        left join
            role r
        on
            rp.role_id = r.id
    </select>
</mapper>

PatternService:

public interface PatternService extends IService<Pattern> {

    List<Pattern> getPatternList();
}

PatternServiceImpl:

@Service
public class PatternServiceImpl extends ServiceImpl<PatternMapper, Pattern> implements PatternService {

    @Autowired
    private PatternMapper patternMapper;

    @Override
    public List<Pattern> getPatternList() {
        return patternMapper.getPatternList();
    }
}

自定义SecurityMetadataSource从数据库读取权限信息

@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private PatternService patternService;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    public static final String ROLE_PREFIX = "ROLE_";

    /**
     * 获取数据库中的资源路径信息
     * @param object 路径信息
     * @return 资源路径信息对应的角色
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //获取请求路径
        String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
        //查询数据库中所有的资源路径(包括所需角色信息)
        List<Pattern> patternList = patternService.getPatternList();

        for (Pattern pattern : patternList) {
            //遍历每一个资源路径和请求路径比较
            if(antPathMatcher.match(pattern.getPattern(), requestURI))
            {
                //匹配成功 获取请求路径需要的角色信息(添加前缀)
                String[] roleList = pattern.getRoleList().stream().map(r -> ROLE_PREFIX + r.getRole()).toArray(String[]::new);
                return SecurityConfig.createList(roleList);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 判断是否支持该类型的数据
     * @param clazz
     * @return
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

Security配置类:

@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserServiceImpl userServiceImpl;

    @Autowired
    private MySecurityMetadataSource mySecurityMetaSource;

    //自定义的AuthenticationManager会覆盖SpringBoot自动配置的AuthenticationManager
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userServiceImpl);
    }

    //将自定义的AuthenticationManager暴露在容器中,使得在其他地方能够注入
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        //配置登录认证请求
        loginFilter.setFilterProcessesUrl("/doLogin");
        //注入自定义的AuthenticationManager
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.OK.value()); //200
            map.put("msg", "登录成功!");
            map.put("authentication", authentication);
            String json = JSON.toJSONString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }));
        loginFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.UNAUTHORIZED.value()); //401
            map.put("msg", "登录失败!!!");
            map.put("exception", exception.getMessage());
            //当产生的异常是SessionAuthenticationException时
            if(exception instanceof SessionAuthenticationException)
            {
                map.put("exception", "该用户已经处于登录状态!!!");
            }
            String json = JSON.toJSONString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            response.flushBuffer();
        }));
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置自定义权限源数据(放在配置的最上面不然可能不生效)
        //1.获取工厂对象
        ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        //2.应用基于Url的权限配置
        http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) 				   {
                        //设置自定义权限源数据
                        object.setSecurityMetadataSource(mySecurityMetaSource);
                        //是否拒绝公共资源(无需权限限制的资源)访问
                        //object.setRejectPublicInvocations(true);
                        return object;
                    }
                });

        http.authorizeRequests()
                .mvcMatchers("/res/**").permitAll()
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/swagger-resources/**","/swagger-ui/**", "/v3/**", "/error").permitAll() //放行swagger
                .mvcMatchers("/vc.png").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                //未认证抛出异常,不显示默认登录页面
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.UNAUTHORIZED.value()); //401
                    response.getWriter().println("请先进行认证!");
                    response.flushBuffer();
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.FORBIDDEN.value()); //403
                    response.getWriter().println("没有权限访问!!!");
                    response.flushBuffer();
                })
                .and()
                .logout(logout -> logout.deleteCookies("JSESSIONID"))
                .logout().logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", "GET"),
                        new AntPathRequestMatcher("/logout", "POST")
                ))
                .logoutSuccessHandler(((request, response, authentication) -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("code", 200);
                    map.put("msg", "注销成功!!!");
                    map.put("authentication", authentication);
                    String json = JSON.toJSONString(map);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(json);
                    response.flushBuffer();
                }))
                .and()
                .csrf().disable();

        //覆盖UsernamePasswordAuthenticationFilter
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

启动测试:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

十二、OAuth

OAUTH(Open Authorization)是一个用于用户资源授权的安全、开放又简易的协议。与以往的授权方式不同

OAUTH不会让第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户名与密码就可以申请

获得该用户资源的授权。

1.四种授权模式

RFC6749文档:https://www.rfc-editor.org/rfc/rfc6749

OAuth授权流程图:

在这里插入图片描述

Client:第三方应用

Resource Owner:资源拥有者(用户)

Authorization Server:授权服务器

Resource Server:资源服务器

步骤:

(A)打开客户端访问第三方应用,需要用户进行授权

(B)用户同意使用信赖的网站进行OAuth授权

©第三方应用向信赖网站的授权服务器申请令牌

(D)授权服务器认证以后发放令牌

(E)第三方应用使用该令牌向信赖网站的资源服务器申请用户相关信息

(F)资源服务器确认令牌无误,将用户信息交给第三方应用

Authorization Code(授权码模式):

功能最完整、流程最严密、最安全并广泛使用的授权模式

在这里插入图片描述

User Agent:用户代理(浏览器)

步骤:

(A)通过浏览器访问第三方应用需要用户进行授权,携带client id、scope、state和redirect uri导向授权服务器

(B)授权服务器验证用户身份并确定用户是否接受或拒绝客户端的授权请求

©如果用户接受授权,授权服务器将导向指定的redirect uri并返回授权码

(D)客户端收到授权码,携带redirect uri,向授权服务器申请令牌

(E)授权服务器核对授权码和redirect uri,如果无误则返回令牌(access token)和更新令牌(refresh token)

https://github.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=read
参数解释
client_id授权服务器注册应用唯一标识
response_type用于请求授权端点,code授权码模式,token简化模式,password密码模式,client_credentials客户端模式
grant_type用于请求令牌端点
重定向uri
scope令牌可以访问的资源(read只读、all读写)
state防止XSRF攻击

Implicit Grant(简化模式):

跳过授权码直接申请令牌

在这里插入图片描述

步骤:

(A)通过浏览器访问第三方应用需要用户进行授权,携带client id、scope、state和redirect uri导向授权服务器

(B)授权服务器验证用户身份并确定用户是否接受或拒绝客户端的授权请求

©如果用户接受授权,授权服务器将用户导向redirect uri并在uri的hash部分(#)包含访问令牌

(D)浏览器请求资源服务器但不携带令牌

(E)资源服务器返回一个网页,其中的脚本可以获取hash中的令牌

(F)浏览器执行脚本提取出令牌

(G)浏览器将令牌发给客户端

https://github.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=read

Resource Owner Password Credentials Grant(密码模式):

在这里插入图片描述

步骤:

(A)用户向客户端提供用户名和密码

(B)客户端将用户名和密码提供给授权服务器请求令牌

©授权服务器认证用户身份无误返回令牌

https://github.com/oauth/authorize?grant_type=password&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=read

Client Credentials Grant(客户端模式)

在这里插入图片描述

步骤:

(A)客户端向授权服务器进行身份验证,并且申请令牌

(B)授权服务器对客户端进行身份验证,如果无误发送令牌

https://github.com/oauth/authorize?grant_type=client_credentials&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=read
2.简单授权登录

OAuth2.0 标准接口:

  • /oauth/authorize授权端点
  • /oauth/token令牌端点
  • /oauth/confirm_access 用户确认授权提交端点
  • /oauth/error授权服务错误信息端点
  • /oauth/check_token资源服务访问的令牌解析端点
  • /oauth/token_key使用JWT令牌提供公钥的端点

创建子模块OAuthTest

导入依赖:

<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>
<!-- oauth客户端 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

Security配置类:

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Login() //开启oauth登录
                .and();
    }
}

控制器:

@RestController
public class TestController {

    /**
    * 获取认证后的用户信息
    */
    @GetMapping("/info")
    public DefaultOAuth2User test(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return (DefaultOAuth2User) authentication.getPrincipal(); 
    }
}

GitHub注册OAuth应用:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

application.yml配置文件:

server:
  port: 8080
spring:
  application:
    name: OAuthTest
  security:
    oauth2:
      client:
        registration:
          github: #client-id、client-secret、redirect-uri必须和注册时的一致
            client-id: xxx
            client-secret: xxx
            redirect-uri: http://localhost:8080/login/oauth2/code/github

启动访问:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Gitee注册OAuth应用:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

application.yml配置文件:

spring:
  security:
    oauth2:
      client:
        registration:
          gitee: # client-id、client-secret、redirect-uri必须和注册时的一致
            provider: gitee
            client-id: xxx
            client-secret: xxx 
            authorization-grant-type: authorization_code # 授权码模式
            redirect-uri: http://localhost:8080/login/oauth2/code/gitee
        provider: # gitee认证需要配置对应的接口路径
          gitee:
            authorization-uri: https://gitee.com/oauth/authorize
            token-uri: https://gitee.com/oauth/token
            user-info-uri: https://gitee.com/api/v5/user
            user-name-attribute: "name"

Gitee官方文档:https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoStargazers?ex=no

启动访问:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.原理分析

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

当用户同意授权后进入attemptAuthentication方法:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.搭建授权 / 资源服务器

常见的授权服务器:

  • keycloak:针对现代应用程序和服务的开源身份和访问管理解决方案,支持单点登录,可以通过 OpenID Connect、OAuth 2.0 等协议对接 Keycloak
  • Apach Oltu:

使用spring cloud搭建授权服务器,spring security搭建资源服务器

内存实现

授权服务器

创建子模块AuthorizationServer

导入依赖:

<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>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

application.yml:

# 应用服务 WEB 访问端口
server:
  port: 8080
# 应用名称
spring:
  application:
    name: AuthorizationServer

Security配置类:

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
    * 配置内存用户
    */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("admin").password("{bcrypt}" + passwordEncoder.encode("admin")).roles("Admin").build());
        return userDetailsService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable();

    }
}

MyInMemoryAuthorizationServer配置类:

@Configuration
@EnableAuthorizationServer //指定本应用为授权服务器
public class MyInMemoryAuthorizationServer extends AuthorizationServerConfigurerAdapter {

    private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 令牌存储在内存中
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .secret("{bcrypt}" + passwordEncoder.encode("secret"))
                .redirectUris("http://www.baidu.com") //重定向URI
                .authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials") //授权码模式、刷新令牌、简化模式、密码模式、客户端模式
                .scopes("read:user"); //令牌允许对用户做的操作
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.userDetailsService(userDetailsService); //配置userDetailsService
        endpoints.authenticationManager(authenticationManager); //配置authenticationManager
    }
}

application.yml:

# 应用服务 WEB 访问端口
server:
  port: 8081
# 应用名称
spring:
  application:
    name: ResourceServer

启动测试:

在这里插入图片描述

在这里插入图片描述

授权码模式:

http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com

在这里插入图片描述

在这里插入图片描述

获取令牌:

curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "grant_type=authorization_code&code=PE1XYu(授权码)&redirect_uri=http://www.baidu.com" http://client:secret@localhost:8080/oauth/token

在这里插入图片描述

返回access_token和refresh_token,过期时间:
在这里插入图片描述

刷新令牌:

curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "grant_type=refresh_token&refresh_token=c3027ec0-c3e0-4924-98bf-9d5e8abbc2ec(refresh_token)&code=JZEVkm&client_id=client" http://client:secret@localhost:8080/oauth/token

在这里插入图片描述

不支持简化模式,密码模式:

获取令牌:

curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "grant_type=password&username=admin&password=admin" http://client:secret@localhost:8080/oauth/token

在这里插入图片描述

刷新令牌:

curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "grant_type=refresh_token&refresh_token=c3027ec0-c3e0-4924-98bf-9d5e8abbc2ec&code=JZEVkm&client_id=client" http://client:secret@localhost:8080/oauth/token

在这里插入图片描述

客户端模式

获取令牌:

curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "grant_type=client_credentials" http://client:secret@localhost:8080/oauth/token

在这里插入图片描述

数据库实现

SQL:https://github.com/spring-attic/spring-security-oauth/blob/main/spring-security-oauth2/src/test/resources/schema.sql

**注意:**需要将LONGVARBINARY类型改为BLOB,如下

create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256) PRIMARY KEY,
  token BLOB,
  authentication blob 
);

create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
	userId VARCHAR(256),
	clientId VARCHAR(256),
	scope VARCHAR(256),
	status VARCHAR(10),
	expiresAt TIMESTAMP,
	lastModifiedAt TIMESTAMP
);


-- customized oauth_client_details table
create table ClientDetails (
  appId VARCHAR(256) PRIMARY KEY,
  resourceIds VARCHAR(256),
  appSecret VARCHAR(256),
  scope VARCHAR(256),
  grantTypes VARCHAR(256),
  redirectUrl VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(256)
);
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
-- 密码为secret 经过bcrypt加密
INSERT INTO `oauth_client_details` VALUES ('client', null, '{bcrypt}$2a$10$OFXjGFb1jCdG9XXH0mAYWuoxnhpDCyYjsLUMmpEqY1695ZN87vEmq', 'read', 'authorization_code,refresh_token,implicit,password,client_credentials', 'http://www.baidu.com', '', null, null, null, null);

在这里插入图片描述

在这里插入图片描述

授权服务器:

添加连接数据库依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

application.yml:

# 应用服务 WEB 访问端口
server:
  port: 8080
# 应用名称
spring:
  application:
    name: AuthorizationServer

  datasource:
    url: jdbc:mysql://localhost:3306/oauth?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver

MyJdbcAuthorizationServer配置类(数据库存储token):

@Configuration
@EnableAuthorizationServer
public class MyJdbcAuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Bean
    public ClientDetailsService jdbcClientDetailsService(){
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(new BCryptPasswordEncoder());
        return jdbcClientDetailsService;
    }

    /**
     * @return jdbc存储token
     */
    @Bean
    public TokenStore tokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager); //配置认证管理器
        endpoints.tokenStore(tokenStore()); //配置令牌存储

        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true); //支持令牌刷新
        defaultTokenServices.setReuseRefreshToken(true); //重复使用刷新令牌

        defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        //令牌过期时间30天
        defaultTokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
        //刷新令牌有效性(以秒为单位)
        defaultTokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(3));

        endpoints.tokenServices(defaultTokenServices); //配置令牌服务
    }
}

资源服务器和授权服务器用同一个数据库避免httpclient网络超时

创建子模块ResourceServer

导入依赖:

<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>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- 资源服务器依赖 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

application.yml:

# 应用服务 WEB 访问端口
server:
  port: 8081
# 应用名称
spring:
  application:
    name: ResourceServer
  datasource:
    url: jdbc:mysql://localhost:3306/oauth?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
@Configuration
@EnableResourceServer
public class MyJdbcResourceServer extends ResourceServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Bean
    public TokenStore tokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore());
    }
}

控制器(受限资源模拟用户信息):

@RestController
public class PrivacyController {
    @GetMapping("/privacy")
    public String privacy(){
        return "privacy data...";
    }
}

启动测试:

先从授权服务器获取token,携带token访问资源服务器:

携带令牌访问受保护资源:

curl -H "Authorization:Bearer 8eb06f4d-8361-46a6-87e4-f9dba8d6df1a" http://localhost:8081/privacy

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Jwt实现

数据库实现需要存储token、authentication等大量数据,使用Jwt不会占用空间

授权服务器

MyJwtAuthorizationServer配置类:

@Configuration
@EnableAuthorizationServer
public class MyJwtAuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Bean
    public ClientDetailsService jdbcClientDetailsService(){
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(new BCryptPasswordEncoder());
        return jdbcClientDetailsService;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("secret"); //设置密钥
        return converter;
    }

    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore())
                .accessTokenConverter(jwtAccessTokenConverter())
                .authenticationManager(authenticationManager);
    }

}

资源服务器

MyJwtResourceServer配置类:

@Configuration
@EnableResourceServer
public class MyJwtResourceServer extends ResourceServerConfigurerAdapter {

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("secret"); //配置相同的密钥
        return converter;
    }

    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore());
    }
}

启动测试:

获取令牌:

在这里插入图片描述

携带令牌访问受保护资源:

在这里插入图片描述

解析JWT:

官网:https://jwt.io/

在这里插入图片描述

不会存储相关数据

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值