Spring Security 简单应用

86 阅读3分钟

准备工作

  1. 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

  1. 引入 pom
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 角色,为了简单我这里是使用的枚举
/**
 * 角色枚举类
 */
@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);
    }

}
  1. 实现 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;
    }
}
  1. 实现 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);
    }

    
}
  1. 添加 请求过滤器
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);
    }
}
  1. 认证失败处理器
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));
    }
    
}
  1. 鉴权失败处理器
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));
    }
}
  1. 添加配置类
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"
}
  1. test接口 不带token
curl --request POST \
  --url http://127.0.0.1:8090/communication-admin/account/test

返回值
{
	"code": 13,
	"msg": "未登录,请登录!",
	"data": null
}
  1. 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
}