日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

基于數據庫自定義UserDetailsService實現JWT認證

作者:lazy_LYF 更新時間: 2022-10-14 編程語言

我的思路是,登錄時使用用戶憑證換取Token,Token存儲在Redis中,每次請求驗證Token與Redis中是否相同并續簽,Redis控制Token過期時間。步驟如下:

添加依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!--身份認證-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>   
    </dependencies>

創建用戶類與角色類

用戶類應該實現UserDetails接口以契合SpringSecurity。

//實現接口時自帶方法,注意各個方法的意義,自行修改,默認為True
@Data
public class User implements UserDetails {
    private Integer id;
    private String phone;
    private Boolean enabled;
    private String username;
    private String password;
    private Integer role;
    private List<Role> roles;
    
	//JsonIgnore在接口傳遞時隱藏敏感信息
    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role:roles)
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        return authorities;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

在實際開發中會設置用戶角色來控制接口訪問,角色類的角色名要求有前綴ROLE_,例如ADMIN,則時ROLE_ADMIN

@Data
public class Role implements GrantedAuthority {
    private Integer id;
    private String roleName;

    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }
}

Redis配置類

先設置常量,例如過期時間。

public final class ConstantKit {
    public static final Integer DEL_FLAG_TRUE=1;
    public static final Integer DEL_FLAG_FALSE=0;
    /**
     * redis存儲token設置的過期時間
     * 單位:秒(1h)
     */
    public static final Long TOKEN_EXPIRE_TIME= Long.valueOf(60*60);

    /**
     * 設置可以重置token過期時間的時間界限
     * 單位:毫秒(30min)
     */
    public static final Long TOKEN_RESET_TIME= Long.valueOf(1000*30*60);
}

創建與屬性文件映射的配置類。

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfigProperty {
    private String host;
    private String password;
    private int port;
    private int database;
    private int timeout;
}

對Jedis進行配置。

public class JedisConfig extends CachingConfigurerSupport {
    private static final Logger LOGGER = LoggerFactory.getLogger(JedisConfig.class);

    @Resource
    private RedisConfigProperty redisConfigProperty;

    @Bean(name = "jedisPoolConfig")
    @ConfigurationProperties(prefix = "spring.redis.pool-config")
    public JedisPoolConfig getRedisConfig(){
        return new JedisPoolConfig();
    }

    @Bean(name = "jedisPool")
    public JedisPool jedisPool(@Qualifier(value = "jedisPoolConfig") final JedisPoolConfig jedisPoolConfig){
        LOGGER.info("Jedis Pool build start");
        String host = redisConfigProperty.getHost();
        int timeout = redisConfigProperty.getTimeout();
        String password = redisConfigProperty.getPassword();
        int database = redisConfigProperty.getDatabase();
        int port = redisConfigProperty.getPort();
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,timeout,password,database);
        LOGGER.info("Jedis Pool build success host={},port={}",host,port);
        return jedisPool;
    }
}

JWT生成驗證

JWT工具類。

public class JwtUtils {

	//PayLoad密鑰
    private static final String JWT_PAYLOAD_USER_KEY = "CSDN";

    /**
     * 生成Token并設置過期時間(過期時間不在此處實現,在Redis中實現)
     * @param userInfo
     * @param privateKey
     * @param expire
     * @return
     */
    public static String generateTokenExpireInMillis(Object userInfo, PrivateKey privateKey,long expire){
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY,JsonUtils.toString(userInfo))
                .setId(createJTI())
//                .setExpiration(new Date(System.currentTimeMillis()+expire))//毫秒
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();

    }

    /**
     * Token解密
     * @param token
     * @param publicKey
     * @return
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey){
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    /**
     * 從Token中獲取個人信息
     * @param token
     * @param publicKey
     * @param userType
     * @return
     * @param <T>
     */
    public static <T>Payload<T> getInfoFromToken(String token,PublicKey publicKey,Class<T> userType){
        Jws<Claims> claimsJws = parserToken(token,publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserinfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(),userType));
        return claims;
    }

    /**
     * 生成ID
     * @return
     */
    private static String createJTI(){
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)));
    }
}

RSA密鑰工具類,用來對Token再加密解密。

public class RsaUtils {
    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 從文件中讀取公鑰
     * @param fileName
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PublicKey getPublicKey(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] bytes = readFile(fileName);
        return getPublicKey(bytes);
    }

    /**
     * 從文件中讀取私鑰
     * @param fileName
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PrivateKey getPrivateKey(String  fileName) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] bytes = readFile(fileName);
        return getPrivateKey(bytes);
    }

    /**
     * 從字節數組中讀取公鑰
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private static PublicKey getPublicKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 從字節數組中讀取私鑰
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getMimeDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 生成密鑰文件
     * @param publicKeyFileName
     * @param privateKeyFileName
     * @param secret
     * @param keySize
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFileName,String privateKeyFileName,String secret,int keySize) throws IOException, NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes(StandardCharsets.UTF_8));
        keyPairGenerator.initialize(Math.max(keySize,DEFAULT_KEY_SIZE),secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFileName,publicKeyBytes);
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFileName,privateKeyBytes);
    }

    /**
     * 讀文件
     * @param fileName
     * @return
     * @throws IOException
     */
    private static byte[] readFile(String fileName) throws IOException {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    /**
     * 寫文件
     * @param destPath
     * @param bytes
     * @throws IOException
     */
    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists())
            dest.createNewFile();
        Files.write(dest.toPath(),bytes);
    }
}

RSA配置類。

Data
@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {
    private String publicKeyPath;
    private String privateKeyPath;
    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct
    public void createKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        this.publicKey = RsaUtils.getPublicKey(publicKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(privateKeyPath);
    }
}

配置文件

在resources文件夾下編輯application.properties。

server.port=8181

# rsa
rsa.key.publicKeyPath=公鑰地址
rsa.key.privateKeyPath=私鑰地址

#redis 基礎配置
# Redis服務器地址
spring.redis.host=127.0.0.1
# Redis服務器連接密碼(默認為空)
spring.redis.password=redis
# Redis數據庫索引(默認為0)
spring.redis.database=0
# Redis服務器連接端口
spring.redis.port=6379
# 連接超時時間(毫秒)
spring.redis.timeout=5000

#redis 連接池配置
#池中最大鏈接數
spring.redis.pool-config.max-total=256
# 連接池中的最大空閑連接
spring.redis.pool-config.max-idle=128
# 連接池中的最小空閑連接
spring.redis.pool-config.min-idle=8
# 調用者獲取鏈接時,是否檢測當前鏈接有效性
spring.redis.pool-config.test-on-borrow=false
# 向鏈接池中歸還鏈接時,是否檢測鏈接有效性
spring.redis.pool-config.test-on-return=false
# 調用者獲取鏈接時,是否檢測空閑超時, 如果超時,則會被移除-
spring.redis.pool-config.test-while-idle=true
# 空閑鏈接檢測線程一次運行檢測多少條鏈接
spring.redis.pool-config.num-tests-per-eviction-run=8

# 緩存
spring.cache.cache-names=c1,c2
spring.cache.redis.time-to-live=1800s

創建認證失敗類

當用戶沒有有效憑證時,會用此類進行處理。

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException{
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","401");
        resultData.put("msg", "請登錄!");
        out.write(new ObjectMapper().writeValueAsString(resultData));
        out.flush();
        out.close();
    }
}

創建權限不足類

當用戶試圖訪問未經授權的接口時,會到這里處理。

public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","403");
        resultData.put("msg", "未授權");
        out.write(new ObjectMapper().writeValueAsString(resultData));
        out.flush();
        out.close();
    }
}

創建過濾器

這里創建兩個過濾器,一個用于登錄頒發token,一個用于續簽鑒定token。

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {;
    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;
    private JedisPool jedisPool;
    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties,
                          JedisPool jedisPool){
        this.authenticationManager = authenticationManager;
        this.rsaKeyProperties = rsaKeyProperties;
        this.jedisPool = jedisPool;
    }

	//首先執行此函數,如果與數據庫中比對不上,則會拋出異常
	//為了防止賬號枚舉,這里只顯示賬號或密碼錯誤
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        User user = null;
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            user = objectMapper.readValue(request.getInputStream(),User.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(),
                            user.userPassword()
                    )
            );
        } catch (Exception e) {
            try {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_OK);
                PrintWriter out = response.getWriter();
                Map<String, Object> map = new HashMap<>();
                map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                map.put("message", "賬號或密碼錯誤!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
                e.printStackTrace();
            }catch (Exception exception){
                exception.printStackTrace();
            }
        }
        return null;
    }

	//登錄成功
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                         Authentication authentication){
        User user = new User();
        user.setUsername(authentication.getName());
        user.setRoles((List<Role>) authentication.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMillis(user,rsaKeyProperties.getPrivateKey(),10*60*1000);
        Jedis jedis = jedisPool.getResource();
        response.addHeader("Authorization","AttackToken "+token);
        String oldToken = jedis.get(user.getUsername());
        if (oldToken!=null)
            jedis.del(oldToken,oldToken+user.getUsername());
        jedis.set(user.getUsername(),token);
        jedis.expire(user.getUsername(), ConstantKit.TOKEN_EXPIRE_TIME);
        jedis.set(token,user.getUsername());
        jedis.expire(token,ConstantKit.TOKEN_EXPIRE_TIME);
        Long currentTime = System.currentTimeMillis();
        jedis.set(token+user.getUsername(),currentTime.toString());
        jedis.close();
        try {
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = response.getWriter();
            Map<String,Object> map = new HashMap<>(4);
            map.put("code",HttpServletResponse.SC_OK);
            map.put("message","登陸成功!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

請求過濾類,對每一個請求的Token進行驗證與續簽。

public class JwtVerifyFilter extends BasicAuthenticationFilter {
    private RsaKeyProperties rsaKeyProperties;
    private JedisPool jedisPool;

    public JwtVerifyFilter(AuthenticationManager authenticationManager,RsaKeyProperties rsaKeyProperties,
                           JedisPool jedisPool) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
        this.jedisPool = jedisPool;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header == null||!header.startsWith("AttackToken ")){
            //不應該在此處拋出異常,拋出異常會重定向至/error,但/error并未放通
            //故會出現只要是出現了異常,如404等都會被AuthenticationEntryPoint捕獲從而返回401
//            throw new AccessDeniedException("未登錄");
            chain.doFilter(request,response);
            return;
        }
        String token = header.replace("AttackToken ","");
        User user = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(),User.class).getUserinfo();
        Jedis jedis = jedisPool.getResource();
        request.setAttribute("requestUser",user.getUsername());
        if (jedis.get(token) == null)
            throw new AccessDeniedException("登錄憑證已廢棄");
        if (user!=null){
            Authentication authentication = new UsernamePasswordAuthenticationToken
                    (user.getUsername(),null,user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            Long tokenBirthTime = Long.valueOf(jedis.get(token+user.getUsername()));
            logger.info("token birth time is:"+tokenBirthTime);
            Long diff = System.currentTimeMillis()-tokenBirthTime;//時間差
            logger.info("token has existed for:"+diff);
            if (jedis.get(user.getUsername())==null)
                throw new AccessDeniedException("登錄過期");
            if (diff> ConstantKit.TOKEN_RESET_TIME){
                jedis.expire(user.getUsername(), ConstantKit.TOKEN_EXPIRE_TIME);
                jedis.expire(token,ConstantKit.TOKEN_EXPIRE_TIME);
                logger.info("Reset expire time success");
                Long newBirthTime = System.currentTimeMillis();
                jedis.set(token+user.getUsername(),newBirthTime.toString());
            }
            jedis.close();
            chain.doFilter(request,response);
        }else {
            throw new AccessDeniedException("未登錄");
        }
    }
}

自定義UserDetailsService

UsernamePasswordAuthenticationToken以及DaoAuthenticationProvider使用UserDetailsService來查詢用戶名、密碼和GrantedAuthority,檢查用戶輸入的密碼是否匹配。

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username", username);
        if (userMapper.selectList(userQueryWrapper).isEmpty()) {
            throw new UsernameNotFoundException("用戶名不存在");
        }
        User user = userMapper.selectList(userQueryWrapper).get(0);
        //將角色賦予用戶
        Role role = roleMapper.selectById(user.getRole());
        List<Role> roles = new ArrayList<>();
        roles.add(roles);
        user.setRoles(roles);
        return user;
    }
}

Spring Security配置類

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserService userService;
    private final RsaKeyProperties rsaKeyProperties;
    private final JedisPool jedisPool;

    public SecurityConfig(UserServiceImpl userServiceImpl, RsaKeyProperties rsaKeyProperties, JedisPool jedisPool) {
        this.userServiceImpl = userServiceImpl;
        this.rsaKeyProperties = rsaKeyProperties;
        this.jedisPool = jedisPool;
    }

    private String[] loadExcludePath() {
        return new String[]{
                "/static/**",
                "/templates/**",
                "/img/**",
                "/js/**",
                "/css/**",
                "/lib/**"
        };
    }

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

    @Bean
    AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {
        AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userServiceImpl)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
        return authenticationManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager, HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                //放通所有靜態資源
                .antMatchers(loadExcludePath()).permitAll()
                //放通注冊
                .antMatchers(HttpMethod.POST,"/user/add").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/logger/**").hasAnyRole("ADMIN","LOGGER")
                .antMatchers("/user/**").hasAnyRole("ADMIN","LOGGER","USER")
                //其余請求都需要認證后訪問
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager,rsaKeyProperties, jedisPool))
                .addFilter(new JwtVerifyFilter(authenticationManager, rsaKeyProperties,jedisPool))
                //已認證但是權限不夠
                .exceptionHandling().accessDeniedHandler(new UserAuthAccessDeniedHandler())
                .and()
                //未能通過認證,也就是未登錄
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session
        return httpSecurity.build();
    }
}

注意事項

  1. 在數據庫中的角色名應該為ROLE_ADMIN,ROLE_LOGGER,ROLE_USER。
  2. 本文對密碼進行了加鹽加密處理,請先對密碼加密后再存儲到數據庫,才能比對成功,加密示例如下
String password = "password";
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encodedPassword = bCryptPasswordEncoder.encode(password);
System.out.println("encodedPassword");
  1. 本文省略了諸多細節,例如Redis、MyBatisPlus等,無法直接使用。

原文鏈接:https://blog.csdn.net/lazy_LYF/article/details/127284982

欄目分類
最近更新