vm+ubuntu/centos(win环境下也行)
docker + redis(自行百度)+Redis Desktop Manager
idea
我们在Spring Initializr中初始化
勾选Spring Web和Spring Security
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.6.RELEASE
com.ssrmj
login-demo
0.0.1-SNAPSHOT
login-demo
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-data-redis
io.jsonwebtoken
jjwt
0.6.0
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.security
spring-security-test
test
org.springframework.boot
spring-boot-maven-plugin
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"
注: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 roles;
}
注:代码中自动生成的serialVersionUID
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();
}
}
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 claims = new HashMap<>(3);
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); // 放入用户名
claims.put(CLAIM_KEY_CREATED, new Date()); // 放入token生成时间
List 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 claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret())
.compact();
}
}
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 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 roles = new ArrayList<>();
private static Role r1 = new Role();
private static Role r2 = new Role();
@Override
public List 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;
}
}
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 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 roles = new ArrayList<>();
for (GrantedAuthority authority : authorities) { // SimpleGrantedAuthority是GrantedAuthority实现类
// GrantedAuthority包含类型为String的获取权限的getAuthority()方法
// 提取角色并放入List中
roles.add(authority.getAuthority());
}
Map 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 authorities = new ArrayList<>(1);
//用于添加用户的权限。将用户权限添加到authorities
List 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);
}
}
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再合适不过了。
注:博主才疏学浅,如有错误,请及时说明,谢谢。