转载

在实战中学习Springboot+Security+redis+jwt的登录流程

一、环境准备

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

二、初始化项目

我们在Spring Initializr中初始化

在实战中学习Springboot+Security+redis+jwt的登录流程

勾选Spring Web和Spring Security

在实战中学习Springboot+Security+redis+jwt的登录流程

在实战中学习Springboot+Security+redis+jwt的登录流程

(一)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"

(三)项目结构

在实战中学习Springboot+Security+redis+jwt的登录流程

(四)model层

注:setter、getter和toString采用lombok

entity.Result(返回结果实体类)

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(自定义状态码)

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(角色)

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(用户)

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

1、BeanConfig(将一些不方便加@Component注解的类放在此处)

什么意思呢,就是有的类我们用@Autowired注入的时候,spring不能识别,于是在这里写成方法注入容器

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);
    }

}

2、JwtConfig(Jwt配置类,将yml中的配置引入)

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;
    }
}

3、WebSecurityConfig(Security拦截配置)

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层

结构图:

在实战中学习Springboot+Security+redis+jwt的登录流程

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 模拟请求

在实战中学习Springboot+Security+redis+jwt的登录流程

点击 Send ,得到响应如下

在实战中学习Springboot+Security+redis+jwt的登录流程

我们利用 Redis Desktop Manager 查看 redis 数据库的情况

在实战中学习Springboot+Security+redis+jwt的登录流程

由于redis是基于内存的数据库,存取速度很快,并且有可持久化的特性,用来存储token再合适不过了。

注:博主才疏学浅,如有错误,请及时说明,谢谢。

在实战中学习Springboot+Security+redis+jwt的登录流程

原文  https://segmentfault.com/a/1190000022359722
正文到此结束
Loading...