SpringBoot+SpringSecurity6+Jwt最小化程式碼
[toc]
零、參考資料
- https://blog.csdn.net/m0_71273766/article/details/132942056
一、快速開始
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 BearerTokenAuthenticationEntryPoint())
// .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
// )
;
return http.build();
}
}
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>