SpringBoot+SpringSecurity6+Jwt最小化代码

SpringBoot+SpringSecurity6+Jwt最小化代码

[toc]

零、参考资料

一、快速开始-认证

1.1、引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cyw</groupId>
    <artifactId>spring-security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-security-demo</name>
    <description>spring-security-demo</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <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.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </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>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

<!-- 以下依赖是为了解决高版本JDK+jjwt库时的报错 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.4.0-b180830.0359</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>3.0.0-M4</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>3.0.0-M4</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>


1.2、JwtUtil


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Date;

@Slf4j
@Component
public class JwtUtil {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 4; //token过期时间,4个小时
    public static final String APP_SECRET = "amRiYzpteXNxbDovL2xvY2FsaG9zdDozMzA2L2RiX3NlY3VyaXR5P3VzZVNTTD1mYWxzZSZzZXJ2ZXJUaW1lem9uZT1VVEMmY2hhcmFjdGVyRW5jb2Rpbmc9dXRmOA=="; //秘钥

    //生成token字符串的方法
    public String getToken(String userName){
        log.info(userName);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("userName", userName)//设置token主体部分 ,存储用户信息
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
    }

    //验证token字符串是否是有效的  包括验证是否过期
    public boolean checkToken(String jwtToken) {
        if(jwtToken == null || jwtToken.isEmpty()){
            log.error("Jwt is empty");
            return false;
        }
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            Claims body = claims.getBody();
            if ( body.getExpiration().after(new Date(System.currentTimeMillis()))){
                return true;
            } else
                return false;
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
    }

    public Claims getTokenBody(String jwtToken){
        if(jwtToken == null || jwtToken.isEmpty()){
            log.error("Jwt is empty");
            return null;
        }
        try {
            return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken).getBody();
        } catch (Exception e){
            log.error(e.getMessage());
            return null;
        }
    }
}

1.3、JwtFilter


import com.cyw.springsecuritydemo.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwtToken = request.getHeader("token");//从请求头中获取token
        if (jwtToken != null && !jwtToken.isEmpty() && jwtUtil.checkToken(jwtToken)){
            try {//token可用
                Claims claims = jwtUtil.getTokenBody(jwtToken);
                String userName = (String) claims.get("userName");
                UserDetails user = myUserDetailsService.loadUserByUsername(userName);
                if (user != null){
                    UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception e){
                log.error(e.getMessage());
            }
        }else {
            log.warn("jwt为空,请确定是否要登录");
        }
        filterChain.doFilter(request, response);//继续过滤
    }
}

1.4、MyUser(UserDetails的实现类)


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;


public class MyUser extends User {
    // todo 其他自定义的字段

    public MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, true,true,true,true,authorities);
    }

    public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
}

1.5、AuthController 登录接口


import com.cyw.springsecuritydemo.pojo.Rst;
import com.cyw.springsecuritydemo.pojo.vo.LoginVo;
import com.cyw.springsecuritydemo.utils.JwtUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class AuthController{
    @Resource
    AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/auth/login")
    public Rst login(LoginVo loginVo) {
        if (loginVo==null || !StringUtils.hasText(loginVo.getUsername())) {
            return Rst.err("用户名或密码不能为空!");
        }

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());

        Authentication authentication = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        MyUser myUser = (MyUser) authentication.getPrincipal();
        String jwtToken = jwtUtil.getToken(myUser.getUsername());
        return Rst.ok("登录成功",jwtToken);
    }
}

1.6、MyUserDetailsService(UserDetailsService的实现类)


import com.cyw.springsecuritydemo.mapper.TbUserMapper;
import com.cyw.springsecuritydemo.pojo.TbUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private TbUserMapper tbUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("进入了 MyUserDetailsService 类");
        TbUser tbUser = tbUserMapper.findUserWithAuthority(username);

        if (tbUser == null) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(tbUser.getAuthorityListStr());
        return new MyUser(username, tbUser.getPwd(), grantedAuthorities);
    }

}

1.8、SpringSecurity的配置类


import com.cyw.springsecuritydemo.auth.JwtFilter;
import com.cyw.springsecuritydemo.auth.MyUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public AuthenticationProvider authenticationProvider(BCryptPasswordEncoder bCryptPasswordEncoder,MyUserDetailsService myUserDetailsService){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(bCryptPasswordEncoder);
        provider.setUserDetailsService(myUserDetailsService);
        return provider;
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationProvider authenticationProvider, JwtFilter jwtFilter) throws Exception {
        http
                .formLogin(AbstractHttpConfigurer::disable) //取消默认登录页面的使用
                .logout(AbstractHttpConfigurer::disable)    //取消默认登出页面的使用
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/auth/login","/auth/register").permitAll() // 允许访问登录接口、注册接口
                        .anyRequest().authenticated()    // 其他接口需要登录才能访问
                )
                .authenticationProvider(authenticationProvider)
                .csrf(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
//                .exceptionHandling((exceptions) -> exceptions
//                        .authenticationEntryPoint(new MyAuthenEntryPoint()) // 认证失败
//                        .accessDeniedHandler(new MyAccessDeniedHandler())   // 授权失败
//                )
                ;
        return http.build();
    }
}

登录表单的配置:

            http            
                .formLogin(form -> {
                    form.usernameParameter("userName")
                        .passwordParameter("pwd")
                        .successHandler(new MyLoginSuccessHandler())
                        .failureHandler(new MyLoginFailHandler());
                })//取消默认登录页面的使用

1.9、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.cyw.springsecuritydemo.mapper.TbUserMapper">

    <resultMap id="rstMap" type="com.cyw.springsecuritydemo.pojo.TbUser">
        <id column="id" property="id"/>
        <result column="user_name" property="userName"/>
        <result column="pwd" property="pwd"/>
        <result column="perm_names" property="authorityListStr"/>
    </resultMap>

    <select id="findUserWithAuthority" resultMap="rstMap">
        SELECT DISTINCT u.id,
                        u.user_name,
                        u.pwd,
                        GROUP_CONCAT(distinct r.role_name SEPARATOR ',') AS role_names,
                        GROUP_CONCAT(distinct p.perm_name SEPARATOR ',') AS perm_names
        FROM tb_user u
                 LEFT JOIN tb_mid_user_role ur ON u.id = ur.uid
                 left join tb_role r on ur.role_id = r.id
                 LEFT JOIN tb_mid_role_perm rp ON ur.role_id = rp.role_id
                 LEFT JOIN tb_perm p ON p.id = rp.perm_id
        <where>
            u.del=0
            <if test="userName != null and userName != ''">
                and user_name = #{userName}
            </if>
        </where>
        GROUP BY u.id
    </select>
</mapper>


1.10、Postman测试

a. POST方式(x-www-form-urlencoded) {{baseUrl}}/auth/login接口
b. 后置操作(接口页签中的Tests选项卡):

let jsonData = pm.response.json();
// console.log(jsonData.data)
pm.collectionVariables.set("jwt", jsonData.data);

c. 其它接口的请求头加一个token头:

  token: {{jwt}}

二、快速开始-授权

a. 在SpringBoot的启动类上加一个注解:


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

// 启用 spring seucrity 的授权功能
@EnableMethodSecurity(prePostEnabled = true) 
@MapperScan("com.cyw.springsecuritydemo.mapper")
@SpringBootApplication
public class SpringSecurityDemoApplication {

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

}

b. 在api接口上设置接口的访问权限:


import com.cyw.springsecuritydemo.pojo.Rst;
import com.cyw.springsecuritydemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author cyw
 * @since 2024-04-16
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    // 假设用户a有'sys:settings:add'权限,用户b没有,则用户a可以访问该接口
    @PreAuthorize("hasAnyAuthority('sys:settings:add')")
    @GetMapping("/hello")
    public Rst user(){
        return Rst.ok("/user/hello 被成功访问了");
    }

}

posted @ 2024-04-16 19:23  卤香味泡面  阅读(298)  评论(0)    收藏  举报