查看原文
其他

Spring Security 真正的前后分离实现

点击关注 👉 java版web项目 2022-04-29

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达


关注公众号后台回复paymall获取实战项目资料+视频

作者:java实用技术

toutiao.com/i6894532348956803597

Spring Security网络上很多前后端分离的示例很多都不是完全的前后分离,而且大家实现的方式各不相同,有的是靠自己写拦截器去自己校验权限的,有的页面是使用themleaf来实现的不是真正的前后分离,看的越多对Spring Security越来越疑惑,此篇文章要用最简单的示例实现出真正的前后端完全分离的权限校验实现。

1. pom.xml

主要依赖是spring-boot-starter-security和jwt。

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

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

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>${jjwt.version}</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>${jjwt.version}</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>${jjwt.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. User

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {

    private Long id;
    private String username;
    private String password;
    private Boolean enabled;
    private List<GrantedAuthority> authorities;


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

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

3. UserDetailsService

@RequiredArgsConstructor
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {


    @Autowired
    private PasswordEncoder passwordEncoder;


    @Override
    public User loadUserByUsername(String username) {

        List<GrantedAuthority> authorities = Arrays.asList(
                new SimpleGrantedAuthority("user:add"),
                new SimpleGrantedAuthority("user:view"),
                new SimpleGrantedAuthority("user:update"));
        User user = new User(1L, username, passwordEncoder.encode("123456"), true, authorities);

        if (user == null) {
            throw new UsernameNotFoundException("用户名或者密码错误");
        }

        return user;
    }
}

4. TokenProvider

/**
 * JWT Token提供器
 */
@Slf4j
@Component
public class TokenProvider implements InitializingBean {

    public static final String AUTHORITIES_KEY = "auth";
    private JwtParser jwtParser;
    private JwtBuilder jwtBuilder;


    @Override
    public void afterPropertiesSet() {
        // 必须使用最少88位的Base64对该令牌进行编码
        String secret = "必须使用最少88位的Base64对该令牌进行编码,一般是配置在application.yml中,需要预先定义好";
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        Key key = Keys.hmacShaKeyFor(keyBytes);
        jwtParser = Jwts.parserBuilder().setSigningKey(key).build();
        jwtBuilder = Jwts.builder().signWith(key, SignatureAlgorithm.HS512);
    }


    public String createToken(Authentication authentication) {
        // 获取权限列表
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        return jwtBuilder
                // 加入ID确保生成的 Token 都不一致
                .setId(UUID.randomUUID().toString())
                // 权限列表
                .claim(AUTHORITIES_KEY, authorities)
                // username
                .setSubject(authentication.getName())
                // 过期时间
                .setExpiration(DateUtils.addDays(new Date(), 1))
                .compact();
    }


    /**
     * 从token中获取认证信息
     * @param token
     * @return
     */
    public Authentication getAuthentication(String token) {
        Claims claims = jwtParser.parseClaimsJws(token).getBody();
        Object authoritiesStr = claims.get(AUTHORITIES_KEY);
        Collection<? extends GrantedAuthority> authorities =
                authoritiesStr != null ?
                        Arrays.stream(authoritiesStr.toString().split(","))
                                .map(SimpleGrantedAuthority::new)
                                .collect(Collectors.toList()) : Collections.emptyList();
        User principal = new User(claims.getSubject(), "******", authorities);
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }
}

5. AccessDeniedHandler

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

   @Override
   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
      // 当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
      response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
   }
}

6. AuthenticationEntryPoint

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401响应
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage());
    }
}

7. TokenFilter

@Slf4j
@Component
public class TokenFilter extends GenericFilterBean {

    private TokenProvider tokenProvider;

    public TokenFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String bearerToken = httpServletRequest.getHeader("Authorization");
        String token = null;
        if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer")) {
            token = bearerToken.replace("Bearer""");
        }

        if (!StringUtils.isEmpty(token)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

8. WebMvcConfigurer

@Configuration
@EnableWebMvc
public class WebMvcConfigurerAdapter implements WebMvcConfigurer {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

9. TokenConfigurer

@RequiredArgsConstructor
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {


    private TokenProvider tokenProvider;

    public TokenConfigurer(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void configure(HttpSecurity http) {
        TokenFilter customFilter = new TokenFilter(tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

10. SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CorsFilter corsFilter;

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private JwtAccessDeniedHandler jwtAccessDeniedHandler;


    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        // 去除 ROLE_ 前缀
        return new GrantedAuthorityDefaults("");
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        // 密码加密方式
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 禁用 CSRF
                .csrf().disable()
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                // 授权异常
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)
                // 防止iframe 造成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()
                // 不创建会话
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 静态资源等等
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/webSocket/**"
                ).permitAll()
                // swagger 文档
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()
                // 文件
                .antMatchers("/avatar/**").permitAll()
                .antMatchers("/file/**").permitAll()
                // 阿里巴巴 druid
                .antMatchers("/druid/**").permitAll()
                // 放行OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 不需要认证的接口
                .antMatchers("/auth/login").permitAll()
                // 所有请求都需要认证
                .anyRequest().authenticated()
                .and().apply(securityConfigurerAdapter());
    }



    private TokenConfigurer securityConfigurerAdapter() {
        return new TokenConfigurer(tokenProvider);
    }
}

11. AuthController

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private AuthenticationManagerBuilder authenticationManagerBuilder;


    @RequestMapping("/login")
    public String login() {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken("monday""123456");
        // 会调用 UserDetailsService.loadUserByUsername
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        String token = tokenProvider.createToken(authentication);
        return token;
    }
}

12. UserController

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/add")
    @PreAuthorize("hasAnyRole('user:add')")
    public String add() {
        return "user:add";
    }

    @RequestMapping("/update")
    @PreAuthorize("hasAnyRole('user:update')")
    public String update() {
        return "user:update";
    }

    @RequestMapping("/view")
    @PreAuthorize("hasAnyRole('user:view')")
    public String view() {
        return "user:view";
    }

    @RequestMapping("/delete")
    @PreAuthorize("hasAnyRole('user:delete')")
    public String delete() {
        return "user:delete";
    }
}

访问有权限的接口。

访问没有权限的接口被拒绝。

13. Spring Security 认证和授权原理

  1. 用户登录会调用UserDetailsService对用户名和密码进行检查,返回用户名、密码、权限字符串列表,认证成功后就会将用户信息放在安全上下文中SecurityContext。
  2. 当用户访问带有权限的接口,Spring Security会调用TokenFilter获取到token,解析token并存入到安全上下文SecurityContext中,然后检查@PreAuthorize("hasAnyRole('user:add')")配置的权限字符串是否在SecurityContext中用户的authorities列表中,如果在表示有权限放行,如果不在表示没有权限,则执行AccessDeniedHandler返回。

有热门推荐👇

从此抛弃理try...catch!

记一次使用 Lombok线上翻车的事故!
Docker安装Jenkins+Shell脚本自动化部署项目
Mybatis 使用的 9 种设计模式,真是太有用了

Java数组转List的三种方式及对比

推荐一款日志切割神器!我常用~

使用 Stream API 高逼格 优化 Java 代码!

Spring MVC请求处理过程不是两张流程图就能讲清楚的

撸一个简易聊天室,不信你学不会实时消息推送(附源码)

最后分享一套微服务电商项目教程(资料笔记+视频):点击阅读全文获取面试资料+项目实战资料(电商/聚合支付)

SPringCloud微服电商完整务教程

1.框架搭建
- 电商项目介绍
- 微服务环境搭建
- 数据库搭建

2.分布式存储系统
- FastDFS原理讲解
- 文件上传
- 文件下载
3.商品发布
- 表结构梳理
- 代码生成器的使用
- 商品增删改
- 商品查询
4.lua,canal实现广告缓存
- 首页广告表设计
- Lua安装使用讲解
- Nginx限流实战
- Canal安装,原理介绍
- Canal同步数据实现
5.索引搜索
- ES安装讲解
- Kibana安装讲解
- DSL语句
- ES API使用
6.商品搜索
- ES 高级搜索功能
- ES 排序规则

 7.Thymeleaf实现静态页面
- Thymeleat 缓存配置讲解
- 搜索页面讲解
8.微服务网关和Jwt令牌
- 微服务网关Zuul/Gateway介绍
- 网关之负载和限流
- 用户服务搭建
- JWT token讲解
- 网关鉴权
9.Spring Security Oauth2
- 单点登陆介绍
- Oauth2介绍
- 共钥私钥讲解
- 加密算法讲解
10.购物车
- 购物车分析和购物车种类分析
- 订单服务创建
- 购物车功能实现
11.订单
- 用户地址测试
- 下单问题分析,幂等
- 用户积分规则
- 二维码生产讲解
- 微信支付流程及模式讲解
12.微信支付
- 微信支付SDK使用讲解
- 微信支付状态查询
- 内网穿透 花生壳
- 微信支付回调
- rabbitMQ 延时队列讲解
13.秒杀基础
- 秒杀需求分析
- 秒杀服务搭建
- 秒杀之Redis
- 秒杀之多线程
14.秒杀核心
- 重复抢单下单问题
- 超卖问题
- 秒杀支付
15.分布式事物
- 分布式事物介绍
- CAP理论介绍
- 2pc/3pc 机制讲解
- TCC事物补偿
- Seata案列讲解
16.高可用集群
- 分布式和集群概念
- Eureka集群介绍
- Redis 集群介绍
- RabbitMq集群安装

点击阅读原文,前往上面微服务电商教程文档

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存