我们在Spring Initializr中初始化
勾选Spring Web和Spring Security
<?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>
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<Role> 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<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(); } }
结构图:
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); }
package com.ssrmj.dao; import com.ssrmj.model.pojo.User; import org.springframework.stereotype.Repository; @Repository public interface UserDao { /** * 根据用户名查询用户 */ User findUserByName(String name); }
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; } }
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; } }
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); } }
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再合适不过了。
注:博主才疏学浅,如有错误,请及时说明,谢谢。