Spring Security——基于前后端分离项目的使用(安全框架)

news2024/11/28 15:57:17

 1.简介

Spring Security有一个过滤器链,也就是说原本在拦截器和过滤器里面做的事都可以用Spring Security完成,比如验证token和将用户id存入线程上下文局部变量等等。

入门案例

创建项目并勾选依赖

最基本的要这两个依赖即可

新建一个Controller层的接口

@RestController
public class HelloController {

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

想要访问这个接口的话会自动跳转到spring security自带的一个登录页面,需要登录之后才能访问别的接口.默认的用户名是user,密码会在控制台输出。

登录之后就可以访问到接口了。 

然后还有一个默认的登出。 

2.认证

登录校验流程

这里就是前后端分离的项目的一个登录流程,包括这个项目也是这样的.

除了登录注册接口之后的请求必须都要携带token。

苍穹外卖day01——项目导入+环境搭建-CSDN博客

SpringSecurity原理

图中有三种类型的过滤器,一个是认证,一个是异常处理,一个是授权。授权那个虽然是写着拦截器,实际也是过滤器。

虽然说这里用的都是过滤器,但是在项目中也是可以拦截器和过滤器一起用的。

 查看具体的过滤器

启动类里面的run方法返回值就是一个spring容器。用debug可以看见有哪些过滤器。能够看见默认过安全过滤器链中有16个过滤器。

认证的流程详解

在认证用的过滤器处理逻辑里面,除了使用了用户名密码认证这个过滤器以外还递归调用了别的一些东西。

虽然这玩意有点像责任链模式,但是完全不是一个东西。

 层层传递调用,最后一个InMemoryUserDetailsManager是在内存中查询用户信息,是UserDetailsManager接口的一个实现类,这个在后面要改成在数据库查询。

第三层那里进行密码比对,正确的话就将权限封装返回给第一层,第一层将其存入一个上下文。

后面的别的过滤器会从上下文获取这个封装信息。

根据需求进行修改

登录的时候

在那个流程图里面最后一层要改成从数据库里面获取用户信息,然后第一层里面也要加多一个响应token的操作,因为这里是前后端分离的项目。所以说要用自定义的Controller代替第一层的默认过滤器。由controller返回token.

所以要做两件事。

1.自定义登录接口 

        调用providerManager , 如果认证通过生成jwt

        把用户信息存入redis中。

2.自定义UserDetailService

        在这个实现类中查询数据库。 

校验的时候 

后续非登录请求如下校验

注意:下面是非登录的请求。

自定义一个jwt过滤器加入过滤器链中。

 这个jwt里面要根据userID查询用户信息,要到数据库查询,每一次请求都查询数据库的话压力过大,所以将信息存redsi里面,直接查询redis.。

所以,可以在登录认证通过的时候将用户信息存入redis.

1.定义jwt认证过滤器

        获取token

        解析token(获取userId)

        从redis获取用户信息

        存入SecurityContextHolder中供别的过滤器使用。

修改前的准备工作

导入redis,jwt和fastjson的依赖。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

这里配置一下redis的序列化,但是不用fastjson配置,不搞那么麻烦。

@Configuration
@Slf4j
public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象");
        RedisTemplate redisTemplate=new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置Redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

配置信息也要写一下。

spring:
  redis:
    host: 
    port: 
    password: 
    database: 1

再定义一个Result风格的响应类。

/**
 * 后端统一返回结果
 * @param <T>
 */
@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

再加上一个jwt和一个响应的工具类。

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}
public class WebUtil {
    public static String readerString(HttpServletResponse response,String string){
        try{
            response.setStatus(1);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }catch(IOException e){
            e.printStackTrace();
        }
        return null;
    }
}

 jwt还需要设定一些参数,这里通过读取配置文件的方式传进来

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 用户生成jwt令牌相关配置
     */
    private String secretKey;
    private long ttl;
    private String tokenName;
}
sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    secret-key: yhy
    # 设置jwt过期时间
    ttl: 7200000
    # 设置前端传递过来的令牌名称
    token-name: token

然后再根据数据库用户表准备一个实体类。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User   implements Serializable {
    private  int id;
    private String username;
    private String password;
    private int role;
}

这里根据实体类去建表,不再提供建表语句。

再次导入依赖

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

最后在启动类加上开启注解式缓存的注解。

@SpringBootApplication
@EnableCaching //开启缓存注解的功能
public class SpringSecurityDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args);
        System.out.println("——————————————");
    }

}

核心代码实现

数据库校验用户 

Pojo层如下 

实现指定的UserDetailService接口,实现要用的方法。

这里因为返回的是一个UserDetail对象,所以要实现一个这个接口,把上面写的User封装进去

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

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

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

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

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

Service层如下 

然后Service实现UserDetailService接口如下所示

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        User user=userMapper.selectByUsername(username);
        //如果没有查询到用户就抛出异常。
        if(Objects.isNull(user)){
            throw new RuntimeException("用户不存在或者密码错误");
        }
        //TODO 查询对应的权限信息

        //把数据封装UserDetail返回
        return new LoginUser(user);
    }
}

mapper层如下

@Mapper
public interface UserMapper {

    @Select("select * from user where username = #{username}")
     User selectByUsername(String usename);
}

现在已经替换了流程图的最后一个部分,前面的登录页面还是默认的,也是可以使用的.

但是登录的话会报错如下。

这里会使用一个默认的PasswordEncoder进行密码,它要求查询得到的密码要在密码前面加上{},括号里面写上标识,如果写上noop的话就说明密码是原文存储。将数据库里面的密码加上{noop}再次登录就可以成功。

密码加密存储 

流程图里面有说到过会通过PasswordEncoder比较两个密码。

所以这里要重写一个配置类替换IOC容器里面默认的PasswordEncoder的Bean对象。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

在测试类里面输出可以看见两次的加密密码不一样,因为它会生成一个随机的盐和原文进行一系列随机的处理之后再进行加密。

@SpringBootTest
public class MapperTest {
    @Test
    void test(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        System.out.println(bCryptPasswordEncoder.encode("123456"));//密码加密
        System.out.println(bCryptPasswordEncoder.encode("123456"));//密码加密
    }

}

  然后再看看另一个方法,原文和密文的密码比对

@SpringBootTest
public class MapperTest {
    @Test
    void test(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        //$2a$10$zG/hDdGw5U6RDG.EgFqhXeb92ErzfvgpghiykM3xSGA9CJtc6Cezm
        //密码比对
        System.out.println(bCryptPasswordEncoder.matches("123456", "$2a$10$zG/hDdGw5U6RDG.EgFqhXeb92ErzfvgpghiykM3xSGA9CJtc6Cezm"));
    }
}

修改数据库里面的密码之后再次尝试去登录也可以登陆进去了

登陆接口

 在配置类当中

新增一个Bean对象,并重写一个configure方法自定义规则。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManager()throws Exception{
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }
}

Controller中

@RestController
public class LoginController {
@Autowired
    private LoginService loginService;
    @PostMapping("/user/login")
    public Result login(@RequestBody User user){
        //登录
        String token = loginService.login(user);
        return Result.success(token);
    }
}

Service中

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public String login(User user) {
        //使用AuthenticationManager进行用户认证
            //先将用户名和密码封装进authentication对象。
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
            //封装完传进来继续调用下一层进行认证。
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //认证没通过,给出相应提示
        if(Objects.isNull(authenticate))
        {
            throw new RuntimeException("登录失败");
        }
        //认证通过,用jwt生成token,将其直接返回。
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        Integer userid=loginUser.getUser().getId();
        String id=userid.toString();
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", id);
        String token = JwtUtil.createJWT(
                jwtProperties.getSecretKey(),
                jwtProperties.getTtl(),
                claims);
        //将完整的用户信息存入redis, id作为key
        updateCache(id,loginUser);
        //返回token给前端
        return token;
    }
    /**
     * 插入和更新缓存数据
     * @param
     */
    private void updateCache(String id,LoginUser loginUser){
        String key="login::"+id;
        redisTemplate.opsForValue().set(key,loginUser);
    }
}

使用postman测试没有问题 

redis里面也正常存入数据 

token认证过滤器实现

这个自定义token过滤器是最先执行的,然后才到spring security的过滤器链,最后才去到Controller控制器方法里面。

@Component
@Slf4j
//默认的过滤器接口有问题,可能会调用多次,所以这里选择继承类
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)){
            //放行
            filterChain.doFilter(request,response); //可能是登录也可能不是,需要放行后交给后面去判断。
            return; //这里是为了获取结果返回的时候不往下执行。
        }
        // 解析token
        int id;
        try {
            Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(),token);
             id = Integer.parseInt(claims.get("id").toString());
            log.info("用户id为:{}",id);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        //从redis获取认证用户信息。
        String redisey="login::"+id;
        LoginUser loginUser=(LoginUser) getValue(redisey);
        System.out.println(loginUser);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request,response);
    }
    private Object getValue(String key){
      return  redisTemplate.opsForValue().get(key);
    }
}

配置认证过滤器

上面写好的过滤器要放到过滤器链最前面的位置。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManager()throws Exception{
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

            //两个参数,一个是过滤器,一个是要添加之前到的过滤器的字节码对象。
            http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);

    }
}

退出登录

获取SecurityContextHolder当中的认证信息,删除redis中对应数据

Controller中

    @RequestMapping("/user/logout")
    public Result logout(){
        loginService.logout();
        return Result.success();
    }

Service中

    @Override
    public void logout() {
        //获取SecurityContextHolder中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser)authentication.getPrincipal();
        int id = loginUser.getUser().getId();
        //删除redis中的值
        String key="login::"+id;
        redisTemplate.delete(key);
    }

 先退出再访问正常资源就会爆出之前自定义的异常

3. 授权

授权基本流程

设置资源所需权限

有基于注解和基于配置两种方法,基于配置的主要用于配置静态资源,在前后端分离项目的后端中已经没有什么静态资源了。

 封装权限信息

在之前写的jwt的token过滤器里面之前只是封装了用户信息,没有权限信息

在启动类上加上注解开启注解配置权限

@EnableGlobalMethodSecurity(prePostEnabled = true)

这里先用硬编码的方式在Controller方法上加上权限注解


@RestController
public class HelloController {

    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')") //调用了一个hasAuthority方法,参数是test,返回值是boolean
    public String hello(){
        return "hello security";
    }
}

 在用户信息的封装对象里面加上权限属性数组permissions,authorities是里面的getAuthorities方法是spring Security在进行权限校验时会自动调用的方法的返回值。

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;

    private List<String> permissions;

    @JSONField(serialize = false) //这个权限信息不能存进redis,会报错
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象,是GrantedAuthority的一个实现类

        if(authorities!=null)
        {
            return authorities;
        }
        //普通实现
//         authorities = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
//            authorities.add(simpleGrantedAuthority);
//        }
        //函数式编程
            authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

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

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

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

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

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

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

 在登录时的最后将权限封装进去。

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        User user=userMapper.selectByUsername(username);
        //如果没有查询到用户就抛出异常。
        if(Objects.isNull(user)){
            throw new RuntimeException("用户不存在或者密码错误");
        }
        //TODO 查询对应的权限信息
        ArrayList<String> strings = new ArrayList<>(Arrays.asList("test", "admin"));
        //把数据封装UserDetail返回
        return new LoginUser(user,strings);
    }
}

在jwt的token过滤器里面获取权限信息封装到authentication中

 

测试需要权限的hello接口,成功拿到响应

将接口上需要的权限改一改,这次就没有响应了

从数据库查询权限信息

RBAC权限模型

RBAC权限模型(Role-Based Access (ontrol) 即:基于色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型.

会有一个用户表,一个权限表,还有一个角色表。不同角色对应的权限都在角色权限关联表

用户和角色的表的关联也有一个用户角色关联表

好麻烦。不想定义这么多表了。 

这里不去实现这部分代码,将来有机会用的时候再来做吧

4.自定义失败处理

将springSecurity的过滤器链中出现的异常一起捕获以固定风格的响应传回前端。

用之前准备的工具类帮助解决

public class WebUtil {
    public static String readerString(HttpServletResponse response,String string){
        try{
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }catch(IOException e){
            e.printStackTrace();
        }
        return null;
    }
}

认证的异常方法

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Result result= Result.error("用户认证失败请查询登录");
        String json = JSON.toJSONString(result);
        //处理异常
        WebUtil.readerString(response,json); //Result风格数据写进response

    }
}

授权的异常方法

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result result= Result.error("权限不足");
        String json = JSON.toJSONString(result);
        //处理异常
        WebUtil.readerString(response,json); //Result风格数据写进response

    }
}

配置异常处理器

回到WebSecurityConfigurerAdapter里面替换默认的两个异常处理器.

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManager()throws Exception{
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                //关闭csrf
                .csrf().disable()//返回一个HttpSecurity对象
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and() //返回一个authorizeRequests对象
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

            //两个参数,一个是过滤器,一个是要添加之前到的过滤器的字节码对象。
            http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);

            //配置异常处理器
            http.exceptionHandling()
                    //配置认证授权失败处理器
                    .authenticationEntryPoint(authenticationEntryPoint)
                    .accessDeniedHandler(accessDeniedHandler);
    }
}

测试两个异常处理器都能正常工作。 

5. 跨域问题解决

微服务项目下一般使用网关做跨域处理

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置运行跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                //是否允许cookie
                .allowCredentials(true)
                //设置允许的请求方式
                .allowedMethods("GET","POST","DELETE","PUT")
                //设置允许的header属性
                .allowedHeaders("*")
                //跨域允许时间
                .maxAge(3600);
    }
}

 然后在SecurityConfiguration要像自定义的token过滤器和异常处理器一样要进行注册使用。

6.其他问题

 其它的权限校验方法

    @PreAuthorize("hasAuthority('test2222')") //调用了一个hasAuthority方法,参数是test,返回值是boolean

 hasAnyAuthority方法传的是一个权限的String数组,用户存在任意一个都可以通过认证。hasAuthority方法内部也是调用的上面的方法,只是它只能传一个权限。

 用法如下

    @PreAuthorize("hasAnyAuthority('admin','test')")

hasRole方法和hasAnyRole是联系起来的,分别是单个角色满足和多个角色,满足其一都可以通过认证。然后调用一个hasAnyAuthorityName方法。 

 hasAnyAuthorityName方法传进去一个默认前缀和定义的权限,需要拼接之后再判断是否有这个权限。默认前缀如下。

 

 然后就要求访问者的权限集合里面也有对应的前缀,这些前缀一般是代表了角色。

    @PreAuthorize("hasRole('test')")
    @PreAuthorize("hasAnyRole('test','admin')")

自定义权限校验方法

在@PreAuthorize方法中返回自定义的权限校验方法,只需要保持方法返回值是true或者false即可

为了能让注解调用到我们自定义的方法,好像要用到一个什么SPEL表达式?

如下定义一个校验方法,从上下文获取用户权限

@Component("ex")
public class YHYExpressionRoot {
    public boolean hasAuthority(String authority){
        //获取当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser  loginUser = (LoginUser)authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

然后 在Controller层里面如下访问。

在SPEL表达式中使用@ex相当于获取容器中名字为ex的bean对象。然后.可以调用对象方法.

@RestController
public class HelloController {

    @RequestMapping("/hello")
    @PreAuthorize("@ex. hasAuthority('test2222')")
    public String hello(){
        return "hello security";
    }
}

基于配置的权限控制

在WebSecurityConfigurerAdapter类继承类SecurityConfiguration里面进行配置。

先写接口方法的路径,然后再写权限,这里后面调用的方法跟用注解时调用的方法是一模一样的。

CSRF

 什么玩意?

登陆成功处理器

前面的代码已经用了自定义的Jwt过滤器完成了用户认证,所以这里就已经不会再执行了这个登录成功处理器了。需要新建一个项目。


@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功");
    }
}

 定义好之后和之前定义的两个异常处理器一样,也要去到WebSecurityConfigurerAdapter进行配置。 

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            //所有请求都要鉴权认证
        http.authorizeRequests().anyRequest().authenticated();

            //配置认证成功处理器
        http.formLogin().successHandler(authenticationSuccessHandler);
    }
}

认证失败处理器

和上面流程一模一样 

@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("认证失败");
    }
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            //所有请求都要鉴权认证
        http.authorizeRequests().anyRequest().authenticated();

            //配置认证成功处理器
        http.formLogin().failureHandler(authenticationFailureHandler);
    }
}

登出成功处理器

一模一样的流程。

@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("注销成功");
    }
}
        //配置注销成功的处理器
        http.logout().logoutSuccessHandler(logoutSuccessHandler);

上面说到的三个处理器都要在使用spring Security的认证方案时才能生效.

其他认证方案畅想

如果还有验证码环节的话可以在最前面再加上一个过滤器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1069866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Dubbo v Spring Cloud:两大技术栈如何选型?

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 提到微服务开源框架&#xff0c;不可不说的是 Dubbo 和 Spring Cloud&#xff0c;这两大框架应该是大家最熟悉的微服务解决方案&#xff0c;也是面试中的热点。今天我们梳理下 Dubbo 和 Spring Cloud 的应用特性&…

win10 U盘安装教程

一年内&#xff0c;第三次重装电脑了&#xff0c;我必须要写一份教程了。从制作U盘开始&#xff0c;到重装系统&#xff0c;全部都记录一下&#xff0c;以备不时之需。 首先&#xff0c;找一个U盘&#xff0c;如果U盘内有需要文件&#xff0c;请自行备份&#xff0c;因为这个U盘…

“双节”出游,VR智慧景区让你的旅游更具“心价比”

中秋国庆八天长假落幕&#xff0c;全国旅游消费市场人气旺、活力足&#xff0c;黄金周旅游消费的变化凝结成为新的文旅趋势&#xff0c;更多的游客会选择人少、景美、价优的错峰游&#xff0c;为了享受品质游玩&#xff0c;VR全景展示为众多游客带来旅游新体验&#xff0c;让你…

分类预测 | MATLAB实现KOA-CNN-BiLSTM开普勒算法优化卷积双向长短期记忆神经网络数据分类预测

分类预测 | MATLAB实现KOA-CNN-BiLSTM开普勒算法优化卷积双向长短期记忆神经网络数据分类预测 目录 分类预测 | MATLAB实现KOA-CNN-BiLSTM开普勒算法优化卷积双向长短期记忆神经网络数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.MATLAB实现KOA-CNN-BiLST…

洗地机怎么选?2023年洗地机推荐

洗地机结合洗地、拖地、扫地的功能&#xff0c;在日常生活中备受关注&#xff0c;他能帮助我们更加节省时间和节省体力&#xff0c;但是面对参差不齐的洗地机市场如何选到适合自己的呢&#xff0c;下文整理了几款非常值得入手的性价比型号&#xff0c;供大家选择参考。 一、CE…

SpringBoot项目创建

创建SpringBoot项目&#xff1a; 选择maven项目&#xff0c;选择Java版本 选择springboot版本&#xff08;这里随便选择&#xff0c;后面会进行更改&#xff0c;用不到这么高的版本&#xff09;&#xff0c;选择所需要的对应工具。 然后下一步即可。 配置修改 &#xff0c;结构…

7346-2015 控制电机基本外形结构型式

声明 本文是学习GB-T 7346-2015 控制电机基本外形结构型式.pdf而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了控制电机的机座号、外形及安装尺寸、轴伸型式、出线方式、标记及铭牌。 本标准适用于各类控制电机(以下简称电机),其…

漏洞复现--中远麒麟堡垒机SQL注入

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

性能监控-微服务链路追踪skywalking搭建

中文文档&#xff1a;hong设置 (skyapm.github.cio) 参考:微服务链路追踪SkyWalking的介绍和部署_skywalking部署_技术闲聊DD的博客-CSDN博客 参考&#xff1a;链路追踪SkyWalking整合项目以及数据持久化_skywalking 持久化_技术闲聊DD的博客-CSDN博客 Liunx部署skywalking以…

python每日一练(2)

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

【debian 12】:debian系统切换中文界面

目录 目录 项目场景 基础参数 原因分析 解决方案 1.ctrlaltT 打开终端 2.查询当前语言环境&#xff08;我的已经设置成了中文 zh_CN.UTF-8&#xff09; 3.打开语言配置界面 4.最后一步&#xff1a;重启 不要放弃任何一个机会&#xff01; 项目场景&#xff1a; 这两…

项目进展(十)-解决ADS1285在调试时出现的问题

一、解决大坑 在项目进展&#xff08;九&#xff09;-完善ADS1285代码这边博客中&#xff0c;看似解决了问题&#xff0c;可以去读数据&#xff0c;但是其实是给自己挖大坑&#xff0c;这边博客就是来填坑的。   首先呢&#xff0c;上篇博客说的是用0x12指令来读取数据&#…

Nebula图数据库

文章目录 摘要引言Nebula图数据库概述Nebula图数据库架构Nebula命令和语法详解连接到Nebula图数据库创建图空间使用图空间创建图插入节点数据插入边数据查询节点数据查询边数据 高级用法索引图计算数据导入和导出事务管理数据备份和恢复安全性管理 Nebula使用实例结论参考文献 …

SwiftUI Spacer() onTapGesture 无法触发

问题&#xff1a;点击这个黑色区域不会 print&#xff0c;黑色区域看上去刚好是 Spacer() 占据的区域 解决办法&#xff1a;不使用 onTapGesture&#xff0c;用 Button 包裹一下 Code: import SwiftUIstruct TestTap: View {var body: some View {NavigationStack {List {Sect…

【框架风格】解释器模式

1、描述 解释器框架风格&#xff08;Interpreter Framework Style&#xff09;是一种软件架构风格&#xff0c;其核心思想是构建一个解释器&#xff08;Interpreter&#xff09;来解释并执行特定领域或问题领域的语言或规则。以下是解释器框架风格的一些特点&#xff1a; 1. 领…

parameterType后面报红

第一种 &#xff1a;parameterType后面报红&#xff0c;点击file——settings 第二种&#xff1a;写全类名

三十、【进阶】B树的演变过程

1、索引结构 &#xff08;1&#xff09;二叉树 &#xff08;2&#xff09;B-Tree树 B-Tree树最大度数为5&#xff0c;代表每一个节点最多存储4个key(每个节点最多存储4个数据)&#xff0c;5个指针(可以指向5个子节点)。 2、演变过程&#xff08;最大度数为5&#xff09; &…

C++ 33.学习C++的意义-狄泰软件学院

一些历史 UNIX操作系统诞生之初是直接用汇编语言编写的随着UNIX系统的发展&#xff0c;汇编语言的开发效率成为瓶颈&#xff0c;所以需要一个新的语言替代汇编语言1971年通过对B语言改良&#xff0c;使其能直接产生机器代码&#xff0c;C语言诞生UNIX使用C语言重写&#xff0c…

音频格式怎么转换成mp3?

音频格式怎么转换成mp3&#xff1f;那是因为很多时候我们在使用手机录音或下载平台音频时&#xff0c;可能会遇到无法打开或播放的情况。这通常是因为我们所使用的音频格式不被通用的播放器所支持&#xff0c;导致了无法打开的问题。为了解决这个问题&#xff0c;我们需要将音频…

“新”国货@2023:质疑、回归与转机

【潮汐商业评论/ 原创】 “我是真爱买国货&#xff0c;上到冰箱电视洗衣机&#xff0c;这样的家电大件儿&#xff0c;下到日化洗护用品&#xff0c;这样的日常小件儿&#xff0c;统统首选国货品牌&#xff0c;也只考虑国货品牌。”此时此刻&#xff0c;Grace正与大家分享着自己…