准备工作
- jwt 工具类
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "ling129RWWWWWlingaaabbbcccddddLING1213aaabb";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
引入Spring Sercuirty
- 引入 pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 角色,为了简单我这里是使用的枚举
/**
* 角色枚举类
*/
@AllArgsConstructor
@Getter
public enum RoleTypeEnum implements IEnum<Integer> {
ROOT(1, "管理员", "ROOT"),
CUSTOMER(3, "客服", "CUSTOMER"),
SYSTEM_ROBOTS(4, "系统机器人", "SYSTEM_ROBOTS"),
USER(5, "普通用户", "USER"),
;
@JsonValue
@EnumValue
private final Integer code;
private final String desc;
private final String roleName;
@Override
public Integer getValue() {
return this.code;
}
public static RoleTypeEnum ofCode(int code) {
return Stream.of(values())
.filter(e -> e.getCode() == code)
.findFirst()
.orElseThrow(UnsupportedOperationException::new);
}
}
- 实现 UserDetails
@Service
@Setter
@Getter
@NoArgsConstructor
public class UserDetailImpl implements UserDetails {
private AccountEntity accountEntity;
// 授权角色
private Collection<GrantedAuthority> authorities;
public UserDetailImpl(AccountEntity accountEntity) {
this.accountEntity = accountEntity;
setAuthorities();
}
public void setAuthorities() {
RoleTypeEnum roleType = accountEntity.getRoleType();
authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + roleType.getRoleName()));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return accountEntity.getPassWord();
}
@Override
public String getUsername() {
return accountEntity.getPhone();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 实现 UserDetailsService
@Service
@RequiredArgsConstructor
public class CommunicationUserDetailService implements UserDetailsService {
private final AccountMapper accountMapper;
/**
* 这里文档说的是根据用户名查询,我这里用的是手机号,可以根据自己的实际需求更改
*/
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
AccountEntity accountEntity = this.getByPhone(phone);
if (Objects.isNull(accountEntity)) {
BizExceptions.throwWithErrorCode(ErrorCodeEnum.USER_LOGIN_ERROR);
}
// 在 UserDetailImpl 设置了角色信息
return new UserDetailImpl(accountEntity);
}
public AccountEntity getByPhone(@NonNull String phone) {
LambdaQueryWrapper<AccountEntity> wrapper = Wrappers.<AccountEntity>lambdaQuery()
.eq(AccountEntity::getPhone, phone);
return accountMapper.selectOne(wrapper);
}
}
- 添加 请求过滤器
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final CommunicationUserDetailService communicationUserDetailService;
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
token = token.substring(7);
String phone = null;
try {
Claims claims = JwtUtil.parseJWT(token);
phone = claims.getSubject();
} catch (Exception e) {
BizExceptions.throwWithErrorCode(ErrorCodeEnum.TOKEN_LAPSE);
}
UserDetails loginUser = communicationUserDetailService.loadUserByUsername(phone);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
- 认证失败处理器
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录失败处理器
*/
@Component
public class CommunicationAuthenticationPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
Logs.error("认证失败: {}", authException);
// 用户未登录
Result<Object> result = Results.ofCommonError(ErrorCodeEnum.USER_NO_LOGIN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Jsons.toJson(result));
}
}
- 鉴权失败处理器
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CommunicationAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Logs.error("鉴权失败: {}", accessDeniedException);
// 无权访问
Result<Object> result = Results.ofCommonError(ErrorCodeEnum.NO_PERMISSION);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Jsons.toJson(result));
}
}
- 添加配置类
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity // 开关
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // securedEnabled: 使用角色校验,prePostEnabled: 前置校验
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final CommunicationAuthenticationPoint authenticationPoint;
private final CommunicationAccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
.authorizeRequests()
.antMatchers("/account/getToken").permitAll() // 允许匿名访问
.antMatchers("/swagger/**", "/v3/**").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().hasAnyRole("ROOT"); // 需要角色ROOT
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
验证
登录获取token, test接口
@Tag(name = "用户信息")
@RestController
@RequestMapping("/account")
@RequiredArgsConstructor
public class AccountController {
private final AccountService accountService;
@Operation(description = "登录-获取token")
@PostMapping("/getToken")
public String getToken(@RequestBody @Validated AccountLoginParams params) {
return accountService.getToken(params);
}
@Operation(description = "test")
@GetMapping("/test")
public Boolean addUser() {
return true;
}
}
@Service
@RequiredArgsConstructor
public class AccountService {
public final AccountMapper accountMapper;
private final PasswordEncoder passwordEncoder;
public AccountEntity getByPhone(@NonNull String phone) {
LambdaQueryWrapper<AccountEntity> wrapper = Wrappers.<AccountEntity>lambdaQuery()
.eq(AccountEntity::getPhone, phone);
return accountMapper.selectOne(wrapper);
}
public String getToken(AccountLoginParams params) {
AccountEntity accountEntity = this.getByPhone(params.getPhone());
if (Objects.isNull(accountEntity)) {
BizExceptions.throwWithErrorCode(ErrorCodeEnum.USER_LONG_PHONE_PASSWORD_ERROR);
}
if (!passwordEncoder.matches(params.getPassword(), accountEntity.getPassWord())) {
BizExceptions.throwWithErrorCode(ErrorCodeEnum.USER_LONG_PHONE_PASSWORD_ERROR);
}
return JwtUtil.createJWT(params.getPhone());
}
}
ROOT 角色获取token
curl --request POST \
--url http://127.0.0.1:8090/communication-admin/account/getToken \
--header 'content-type: application/json' \
--data '{
"phone": "13612345678",
"password": "com_123"
}'
返回值
{
"code": 0,
"msg": "success",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlYTAyMzhiMTVmNzA0ZTNhODI3NDQ1ZTMzMGNjMWNmYyIsInN1YiI6IjEzNjEyMzQ1Njc4IiwiaXNzIjoic2ciLCJpYXQiOjE3MDE4NzE4MDksImV4cCI6MTcwMzA4MTQwOX0.xuDikcN9Rlt19Tiy_LlUJvjBaVfjosU-WLq-JFDpl_M"
}
- test接口 不带token
curl --request POST \
--url http://127.0.0.1:8090/communication-admin/account/test
返回值
{
"code": 13,
"msg": "未登录,请登录!",
"data": null
}
- test接口 带token
curl --request GET \
--url http://127.0.0.1:8090/communication-admin/account/test \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlYTAyMzhiMTVmNzA0ZTNhODI3NDQ1ZTMzMGNjMWNmYyIsInN1YiI6IjEzNjEyMzQ1Njc4IiwiaXNzIjoic2ciLCJpYXQiOjE3MDE4NzE4MDksImV4cCI6MTcwMzA4MTQwOX0.xuDikcN9Rlt19Tiy_LlUJvjBaVfjosU-WLq-JFDpl_M'
返回值
{
"code": 0,
"msg": "success",
"data": true
}
普通用户获取token
curl --request POST \
--url http://127.0.0.1:8090/communication-admin/account/getToken \
--header 'content-type: application/json' \
--data '{
"phone": "17612345678",
"password": "com_123"
}'
返回值
{
"code": 0,
"msg": "success",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjNWVkZGViMGQwZDk0MmVjOWM4NWZjZmVkOTg5MjI3MiIsInN1YiI6IjE3NjEyMzQ1Njc4IiwiaXNzIjoic2ciLCJpYXQiOjE3MDIyOTk0MzQsImV4cCI6MTcwMzUwOTAzNH0.gPEd6z4xfUXPWw8NHxHgonrQHphVu7uZqXDq-r5WP6Y"
}
test接口
curl --request GET \
--url http://127.0.0.1:8090/communication-admin/account/test \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjNWVkZGViMGQwZDk0MmVjOWM4NWZjZmVkOTg5MjI3MiIsInN1YiI6IjE3NjEyMzQ1Njc4IiwiaXNzIjoic2ciLCJpYXQiOjE3MDIyOTk0MzQsImV4cCI6MTcwMzUwOTAzNH0.gPEd6z4xfUXPWw8NHxHgonrQHphVu7uZqXDq-r5WP6Y'v
{
"code": 14,
"msg": "无权访问",
"data": null
}