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

 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的认证方案时才能生效.

其他认证方案畅想

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值