在實戰中學習Springboot+Security+redis+jwt的登陸流程

1、環境準備

  • vm+ubuntu/centos(win環境下也行)
  • docker + redis(自行百度)+Redis Desktop Manager
  • idea

2、初始化項目

咱們在Spring Initializr中初始化java

在这里插入图片描述

勾選Spring Web和Spring Securitylinux

在这里插入图片描述

在这里插入图片描述

(一)pom.xml

<?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>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ssrmj</groupId>
    <artifactId>login-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>login-demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- spring-security -->
        <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>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>

        <!-- lombok -->
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

(二)yml配置

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false

  ###Redis
  redis:
    host: linux的ip
    port: 6379
    timeout: 2000ms
    password: redis密碼 #密碼
    jedis:
      pool:
        max-active: 10
        max-idle: 8
        min-idle: 2
        max-wait: 1000ms

logging:
  level:
    org.springframework.security: info
    root: info
  path: e:/log/login-demo-log

### jwt
jwt:
  ###過時時間 單位s
  time: 1800
  ###安全密鑰
  secret: "BlogSecret"
  ###token前綴
  prefix: "Bearer "
  ###http頭key
  header: "Authorization"

(三)項目結構

在这里插入图片描述

(四)model層

:setter、getter和toString採用lombok
entity.Result(返回結果實體類)web

package com.ssrmj.model.entity;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.ToString;

/**
 * @Description: 返回結果實體類
 * @Author: Mt.Li
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@ToString
public class Result {

    private Integer code; // 返回狀態碼

    private String message; // 返回信息

    private Object data; // 返回數據

    private Result(){

    }

    public Result(Integer code, String message) {
        super();
        this.code = code;
        this.message = message;
    }

    public Result(Integer code, String message, Object data) {
        super();
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static Result create(Integer code, String message){
        return new Result(code,message);
    }

    public static Result create(Integer code, String message, Object data){
        return new Result(code,message,data);
    }
}

entity.StatusCode(自定義狀態碼)redis

package com.ssrmj.model.entity;

/**
 * 自定義狀態碼
 */
public class StatusCode {
    // 操做成功
    public static final int OK = 200;
    
    // 失敗
    public static final int ERROR = 201;
    
    // 用戶名或密碼錯誤
    public static final int LOGINERROR = 202;
    
    // token過時
    public static final int TOKENEXPIREE = 203;
    
    // 權限不足
    public static final int ACCESSERROR = 403;
    
    // 遠程調用失敗
    public static final int REMOTEERROR = 204;
    
    // 重複操做
    public static final int REPERROR = 205;
    
    // 業務層錯誤
    public static final int SERVICEERROR = 500;
    
    // 資源不存在
    public static final int NOTFOUND = 404;

}

pojo.Role(角色)spring

package com.ssrmj.model.pojo;

import lombok.Data;
import lombok.ToString;

/**
 * @Description: 角色
 * @Author: Mt.Li
 */

@Data
@ToString
public class Role {

    private Integer id;//角色id
    private String name;//角色名
    
}

pojo.User(用戶)docker

package com.ssrmj.model.pojo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;
import java.util.List;

/**
 * @Description: 用戶
 * @Author: Mt.Li
 */

@Data
@ToString
public class User implements Serializable {

    // 自動生成的serialVersionUID
    private static final long serialVersionUID = 7015283901517310682L;

    private Integer id;

    private String name;

    private String password;
    
    // 用戶狀態,0-封禁,1-正常
    private Integer state;

    @JsonIgnore
    private List<Role> roles;

}

:代碼中自動生成的serialVersionUID數據庫

(五)config

一、BeanConfig(將一些不方便加@Component註解的類放在此處)
什麼意思呢,就是有的類咱們用@Autowired注入的時候,spring不能識別,因而在這裏寫成方法注入容器apache

package com.ssrmj.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 將一些不方便加@Component註解的類放在此處加入spring容器
 */

@Component
public class BeanConfig {

    /**
     * spring-security加密方法
     */
    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * spring-boot內置的json工具
     */
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

}

二、JwtConfig(Jwt配置類,將yml中的配置引入)json

package com.ssrmj.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtConfig {
    public static final String REDIS_TOKEN_KEY_PREFIX = "TOKEN_";
    private long time;     // 過時時間
    private String secret; // JWT密碼
    private String prefix; // Token前綴
    private String header; // 存放Token的Header Key

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }
}

三、WebSecurityConfig(Security攔截配置)ubuntu

package com.ssrmj.config;

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;

/**
 * @Description:
 * @Author: Mt.Li
 */

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 開啓Spring方法級安全,開啓前置註解,一樣也是開啓了Security註解模式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        //禁用csrf
        //options所有放行
        //post 放行
        httpSecurity.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers(HttpMethod.POST).permitAll() // 爲了方便測試,放行post
                .antMatchers(HttpMethod.PUT).authenticated()
                .antMatchers(HttpMethod.DELETE).authenticated()
                .antMatchers(HttpMethod.GET).authenticated();

        httpSecurity.headers().cacheControl();
    }

}

六)util

JwtTokenUtil(關於token操做的工具類)

package com.ssrmj.util;

import com.ssrmj.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.*;

@Component
public class JwtTokenUtil implements Serializable {
    
    private static final long serialVersionUID = 7965205899118624911L;

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_ROLES = "roles";
 
    @Autowired
    private JwtConfig jwtConfig;

    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = new Date((Long)claims.get(CLAIM_KEY_CREATED));
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    /**
     * 從token中獲取過時時間
     */
    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(jwtConfig.getSecret())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成過時時間 單位[ms]
     *
     */
    private Date generateExpirationDate() {
        // 當前毫秒級時間 + yml中的time * 1000
        return new Date(System.currentTimeMillis() + jwtConfig.getTime() * 1000);
    }

    /**
     * 根據提供的用戶詳細信息生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(3);
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); // 放入用戶名
        claims.put(CLAIM_KEY_CREATED, new Date()); // 放入token生成時間
        List<String> roles = new ArrayList<>();
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        for (GrantedAuthority authority : authorities) { // SimpleGrantedAuthority是GrantedAuthority實現類
            // GrantedAuthority包含類型爲String的獲取權限的getAuthority()方法
            // 提取角色並放入List中
            roles.add(authority.getAuthority());
        }
        claims.put(CLAIM_KEY_ROLES, roles); // 放入用戶權限

        return generateToken(claims);
    }

    /**
     * 生成token(JWT令牌)
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret())
                .compact();
    }

}

(七)dao層

結構圖:
在这里插入图片描述

RoleDao

package com.ssrmj.dao;

import com.ssrmj.model.pojo.Role;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface RoleDao {
    /**
     * 根據用戶id查詢角色
     */
    List<Role> findUserRoles(Integer id);

}

UserDao

package com.ssrmj.dao;

import com.ssrmj.model.pojo.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserDao {

    /**
     * 根據用戶名查詢用戶
     */
    User findUserByName(String name);

}

RoleDaoImpl

package com.ssrmj.dao.impl;

import com.ssrmj.dao.RoleDao;
import com.ssrmj.model.pojo.Role;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description:
 * @Author: Mt.Li
 */
@Service
public class RoleDaoImpl implements RoleDao {

    private List<Role> roles = new ArrayList<>();
    private static Role r1 = new Role();
    private static Role r2 = new Role();

    @Override
    public List<Role> findUserRoles(Integer id) {
        if(id == 1) {
            r1.setId(0);
            r1.setName("ADMIN");
            r2.setId(1);
            r2.setName("USER");

            roles.add(r1);
            roles.add(r2);
            return roles;
        }
        return null;
    }
}

UserDaoImpl

package com.ssrmj.dao.impl;

import com.ssrmj.dao.UserDao;
import com.ssrmj.model.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Description:
 * @Author: Mt.Li
 */
@Service
public class UserDaoImpl implements UserDao {

    @Autowired
    RoleDaoImpl roleDaoImpl;

    @Override
    public User findUserByName(String name) {
        User user = new User();
        user.setId(1);
        user.setName("admin");
        user.setPassword("123456");
        user.setState(1);
        user.setRoles(roleDaoImpl.findUserRoles(user.getId()));
        return user;
    }
}

(八)service

LoginService

package com.ssrmj.service;

import com.ssrmj.config.JwtConfig;
import com.ssrmj.dao.impl.RoleDaoImpl;
import com.ssrmj.dao.impl.UserDaoImpl;
import com.ssrmj.model.pojo.Role;
import com.ssrmj.model.pojo.User;
import com.ssrmj.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @Description:
 * @Author: Mt.Li
 */

@Service
public class LoginService implements UserDetailsService {

    @Autowired
    UserDaoImpl userDao;

    @Autowired
    RoleDaoImpl roleDao;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private JwtConfig jwtConfig;

    public Map login(User user) throws RuntimeException{
        User dbUser = this.findUserByName(user.getName());
        // 用戶不存在 或者 密碼錯誤
        if (dbUser == null || !dbUser.getName().equals("admin") || !dbUser.getPassword().equals("123456")) {
            throw new UsernameNotFoundException("用戶名或密碼錯誤");
        }

        // 用戶已被封禁
        if (0 == dbUser.getState()) {
            throw new RuntimeException("你已被封禁");
        }

        // 用戶名 密碼匹配,獲取用戶詳細信息(包含角色Role)
        final UserDetails userDetails = this.loadUserByUsername(user.getName());

        // 根據用戶詳細信息生成token
        final String token = jwtTokenUtil.generateToken(userDetails);
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        List<String> roles = new ArrayList<>();
        for (GrantedAuthority authority : authorities) { // SimpleGrantedAuthority是GrantedAuthority實現類
            // GrantedAuthority包含類型爲String的獲取權限的getAuthority()方法
            // 提取角色並放入List中
            roles.add(authority.getAuthority());
        }

        Map<String, Object> map = new HashMap<>(3);

        map.put("token", jwtConfig.getPrefix() + token);
        map.put("name", user.getName());
        map.put("roles", roles);

        //將token存入redis(TOKEN_username, Bearer + token, jwt存放五天 過時時間) jwtConfig.time 單位[s]
        redisTemplate.opsForValue().
                set(JwtConfig.REDIS_TOKEN_KEY_PREFIX + user.getName(), jwtConfig.getPrefix() + token, jwtConfig.getTime(), TimeUnit.SECONDS);

        return map;

    }

    /**
     * 根據用戶名查詢用戶
     */
    public User findUserByName(String name) {
        return userDao.findUserByName(name);
    }

    /**
     * 根據用戶名查詢用戶
     */
    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        User user = userDao.findUserByName(name);
        // 新建權限集合,SimpleGrantedAuthority是GrantedAuthority實現類
        List<SimpleGrantedAuthority> authorities = new ArrayList<>(1);
        //用於添加用戶的權限。將用戶權限添加到authorities
        List<Role> roles = roleDao.findUserRoles(user.getId()); // 查詢該用戶的角色
        for (Role role : roles) {
            // 將role的name放入權限的集合
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return new org.springframework.security.core.userdetails.User(user.getName(), "***********", authorities);
    }

}

(九)controller

UserController

package com.ssrmj.controller;

import com.ssrmj.model.entity.Result;
import com.ssrmj.model.entity.StatusCode;
import com.ssrmj.model.pojo.User;
import com.ssrmj.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @Description:
 * @Author: Mt.Li
 */

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private LoginService loginService;

    /**
     * 登陸返回token
     */
    @PostMapping("/login")
    public Result login(User user) {

        try {
            Map map = loginService.login(user);
            return Result.create(StatusCode.OK, "登陸成功", map);
        } catch (UsernameNotFoundException e) {
            return Result.create(StatusCode.LOGINERROR, "登陸失敗,用戶名或密碼錯誤");
        } catch (RuntimeException re) {
            return Result.create(StatusCode.LOGINERROR, re.getMessage());
        }
    }

}

測試

測試咱們用postman模擬請求

在这里插入图片描述

點擊Send,獲得響應以下

在这里插入图片描述

咱們利用Redis Desktop Manager查看redis數據庫的狀況

在这里插入图片描述

因爲redis是基於內存的數據庫,存取速度很快,而且有可持久化的特性,用來存儲token再合適不過了。
注:博主才疏學淺,若有錯誤,請及時說明,謝謝。

gif5新文件(1).gif

相關文章
相關標籤/搜索