IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> spring-boot + spring-security + jwt 实现前后端分离的权限管理 -> 正文阅读

[Java知识库]spring-boot + spring-security + jwt 实现前后端分离的权限管理

一、搭建环境

java中常用的权限管理框架有 shiro 和 spring security,之前一直在用 shiro 管理权限,但是后来发现 shiro 确实和前后端分离不太搭,就来研究了两天spring security,与 shiro 不同的是,spring security 是通过一系列的 过滤链管理权限的,而且这些过滤器都可以自定义,虽然比 shiro 体量更大,但是更加的灵活,可以高度自定义,而且 spring security 还会自动生成 login 接口。

1.1 导入依赖和基本配置

<dependencies>
<!--        java-jwt依赖-->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.11.0</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
    </dependency>
    
    <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>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--mybatis-plus依赖-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.1</version>
    </dependency>
    <!--模板引擎-->
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.2</version>
    </dependency>
    <!--自动生成代码时会用到的依赖-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.4.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.3</version>
        <scope>compile</scope>
    </dependency>
</dependencies>
server:
  port: 9999
spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver            # mysql驱动包 com.mysql.jdbc.Driver
    url: {your url}
    username: {your username}
    password: {your password}

mybatis-plus:
  mapper-locations: classpath:com/gewj/mapper/xml/*

1.2、 数据库和简单实体类创建

create table user
(
    id        bigint auto_increment comment 'userId'
        primary key,
    username  varchar(50)                not null comment '用户名',
    password  varchar(1024)              not null comment '密码',
    nickname  varchar(50)                null comment '昵称',
    telepnone varchar(20)                null,
    email     varchar(30)                null comment '邮箱',
    role      varchar(10) default 'ROLE_USER' null
);

创建实体类可以用 mybatis-plus 的代码生成器,也可以手动写,此处就不赘述啦

需要注意的是,userService 里面最好有一个 getUserByUsername() 的方法

public User getByUsername(String username) {
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("username",username);
    User one = getOne(wrapper);
    return one;
}

1.3、 创建 UserDetails 类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetials extends User implements UserDetails {


    private Long id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    
    //下面的变量不是必须的
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;



    public UserDetials(User user, Collection<? extends GrantedAuthority> authorities) {
        this.setUsername(user.getUsername());
        this.setId(user.getId());
        this.setPassword(user.getPassword());
        this.setAuthorities(authorities);
        this.setAccountNonExpired(true);
        this.setAccountNonLocked(true);
        this.setCredentialsNonExpired(true);
        this.setEnabled(true);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return 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 true;
    }

}

这个类的作用是提供给 spring security的。

Result 类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
    private Integer code;
    private String  message;
    private Boolean success;
    private Map<String, Object> data = new HashMap<>();

    public static Result ok(String message) {
        Result result = new Result();
        result.setCode(200);
        result.setMessage(message);
        result.setSuccess(true);

        return result;
    }

    public static Result error(Integer code, String message) {
        Result result = new Result();
        result.setSuccess(false);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static Result ok() {
        Result result = new Result();
        result.setCode(200);
        result.setSuccess(true);

        return result;
    }

    public static Result error() {
        Result result = new Result();
        result.setCode(500);
        return result;
    }

    public static Result error(Integer code) {
        Result result = new Result();
        result.setCode(code);
        return result;
    }

    public Result message(String message) {
        this.setMessage(message);
        return this;
    }

    public Result data(Map<String, Object> map) {
        this.setData(map);
        return this;
    }

    public Result data(String key, Object value) {
        this.data.put(key, value);
        return this;
    }

    public Result(Integer code) {
        this.code = code;
    }

    public Result code(Integer code) {
        this.setCode(code);
        return this;
    }

}

1.4、 jwt 工具类

@Component
public class JwtUtil {

//    过期时长(分钟);
    private static final int EXPIRE_TIME_MINUTE = 300;
    private static final String secret = "zlgewj";

    public static String sign(String username, String authorities) {

        Date date = DateUtil.offsetMinute(new Date(),EXPIRE_TIME_MINUTE);

        String jwt = JWT.create()
                .withClaim("username",username)
                .withClaim("currentTimeMillis", String.valueOf(date))
                .withClaim("authorities",authorities)
                .withExpiresAt(date)
                .sign(Algorithm.HMAC256(secret));
        return jwt;
    }

    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    public static boolean verify(String token, String username) {
        try {
            DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secret))
                    .withClaim("username", username)
                    .build()
                    .verify(token);
            return true;
        }catch (Exception e) {
            return false;
        }
    }

    public static String getClaim(String token, String claimName) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(claimName).asString();
        }catch (JWTDecodeException e){
            e.printStackTrace();
            return null;
        }
    }

}

二、 filter 和 handler

下面正头戏才开始:

2.1、 登录过滤器

@Slf4j
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {

    
    public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {

        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }

	//发送登录请求会执行的方法
    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {

        log.info("执行了 attemptAuthentication 方法");
        UserDetials userDetials = new ObjectMapper().readValue(httpServletRequest.getInputStream(), UserDetials.class);

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                userDetials.getUsername(),
                userDetials.getPassword(),
                userDetials.getAuthorities()
        );
        //调用 AuthenticationManager 的 authenticate 方法验证,我们传了一个 UsernamePasswordAuthenticationToken
        return getAuthenticationManager().authenticate(token);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

        log.info("执行了登陆成功回调");

        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();

        StringBuffer stringBuffer = new StringBuffer();

        authorities.forEach(authority -> {
            stringBuffer.append(authority.getAuthority()).append(",");
        });

        String sign = JwtUtil.sign(authResult.getName(),stringBuffer.toString());
        response.setContentType("application/json; charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();

        UserVo userVo = new UserVo();
        userVo.setToken(sign);

        Result result = Result.ok().data("user", userVo).message("登陆成功!");
        outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));

        outputStream.flush();
        outputStream.close();

    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setContentType("application/json; charset=UTF-8");

        log.info("执行了登录失败回调");
        Result res = Result.error(403, "账号或密码错误~");

        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(JSONUtil.toJsonStr(res).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

发送的登录请求会来到 AbstractAuthenticationProcessingFilter 这个过滤器,我们实现了这个过滤器,调用getAuthenticationManager().authenticate(token); 这个时候,token 里面还没有 authorities, spring security 会用 UserDetailsService 查询用户的详细信息

2.2、 授权过滤器

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = request.getHeader("USER_TOKEN");

        Authentication authentication = null;

            if (token != null) {
                List<SimpleGrantedAuthority> authorities = Arrays.stream(JwtUtil.getClaim(token, "authorities").toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList()
                        );
                String username = JwtUtil.getUsername(token);
                log.info("认证过滤器执行了,当前用户权限:{}",authorities.toString());

                if (username != null) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
                    usernamePasswordAuthenticationToken.setDetails(token);
                    
                    if (!JwtUtil.verify(token, username)) {
                        log.info("token验证失败~");
                        response.setContentType("application/json; charset=UTF-8");
                        ServletOutputStream outputStream = response.getOutputStream();
                        Result error = Result.error(403, "登录过时,请重新登录~");
                        outputStream.write(JSONUtil.toJsonStr(error).getBytes(StandardCharsets.UTF_8));
                        outputStream.flush();
                        outputStream.close();
                    }
                    
                    authentication = usernamePasswordAuthenticationToken;
                }
            }
            SecurityContextHolder.getContext().setAuthentication(authentication);

            filterChain.doFilter(request, response);


    }
}

这个方法会根据请求头里 token 的 权限列表拿出来,放到全局,用我们 JwtUtil.verify 方法认证

2.3、权限不足的处理

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        response.setContentType("application/json; charset=UTF-8");

        ServletOutputStream outputStream = response.getOutputStream();
        Result error = Result.error(403, "权限不足~");
        outputStream.write(JSONUtil.toJsonStr(error).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();

    }
}

当请求的权限不足的时候就会来到这里

2.4、 获取 UserDetails 的 service

@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService {


    private static UserService userService;

    @Autowired
    public void setUserService(UserService userService1) {
        userService = userService1;
    }

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

        User byUsername = userService.getByUsername(username);
        if ( null == byUsername) {
            throw new UsernameNotFoundException("用户名或密码不正确");
        }
        UserDetials userDetials = new UserDetials(byUsername,getUserAuthority(byUsername.getId()));

        System.out.println(userDetials.toString());
        return userDetials;
    }


    // 根据 userId 获取用户的角色列表, 含有多个角色的用 ,分隔
    public List<GrantedAuthority> getUserAuthority(Long userId){

        // 角色(ROLE_ADMIN)
        User user = userService.getById(userId);  // ROLE_ADMIN,ROLE_NORMAL
        String role = user.getRole();
        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
    }

}

执行 getAuthenticationManager().authenticate(token); 方法的时候 loadUserByUsername() 这个方法就会被调用,所以我们需要重写这个方法

三、配置类

刚刚我们写的 filter 和 handler,spring security 只知道它们的存在,不知道该不该调用,什么时候调用,所以

@Configuration
@EnableWebSecurity
//开启精确到方法的注解支持
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    public static String ADMIN = "ROLE_ADMIN";
    public static String USER = "ROLE_USER";
    @Autowired
    private MyLogoutHandler myLogoutHandler;
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    /**
     * 开放访问的请求
     */
    private final static String[] PERMIT_ALL_MAPPING = {
            "/api/hello",
            "/api/login",
            "/api/home",
            "/api/verifyImage",
            "/api/image/verify",
            "/images/**"
    };


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

    /**
     * 跨域配置
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        // 允许跨域访问的 URL
        List<String> allowedOriginsUrl = new ArrayList<>();
        allowedOriginsUrl.add("*");

        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置允许跨域访问的 URL
        config.setAllowedOrigins(allowedOriginsUrl);
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(PERMIT_ALL_MAPPING)
                .permitAll()
                .antMatchers("/api/user/**", "/api/data", "/api/logout")
                // USER 和 ADMIN 都可以访问
                .hasAnyAuthority(USER, ADMIN)
                .antMatchers("/api/admin/**")
                // 只有 ADMIN 才可以访问
                .hasAnyAuthority(ADMIN)
                .anyRequest()
                .authenticated()
                .and()
                // 添加过滤器链,前一个参数过滤器, 后一个参数过滤器添加的地方
                // 登陆过滤器
                .addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                // 请求过滤器
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 开启跨域
                .cors()
                .and()
                .csrf()
                .disable()
                .logout()
                .addLogoutHandler(myLogoutHandler)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中写入用户数据
        auth.
                authenticationProvider(daoAuthenticationProvider());
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {

        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setHideUserNotFoundExceptions(false);
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(new UserDetailsServiceImpl());
        return provider;
    }
}

再来个测试接口测试一下

@RestController()
@RequestMapping("/api")
public class TestController {

    @GetMapping("/hello")
    //只有 ROLE_ADMIN 角色可以访问
    @Secured({"ROLE_ADMIN"})
    public String hello() {
        return "hello";
    }

}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-09-21 00:12:55  更:2022-09-21 00:14:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年3日历 -2025/3/10 14:43:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码