简介
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可 以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security
模块,进行少量的配置,即可实现强大的安全管理!
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
认证: 验证当前访问系统的是不是本系统用户,并且要确认具体是哪个用户。
授权:也就是用户是否有权限进行某个操作
快速入门
1、创建一个springboot工程,并添加SpingSecurity
相应依赖
<!--SpringSecurity的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2、编写一个控制器,尝试访问
@RestController public class HelloController { @RequestMapping("/hello") public String hello(){ return "hello"; } }
运行程序进行访问,我们会发现浏览器会自动跳转到一个登陆的页面,也就是我们必须先要进行登陆
用户名默认为user,密码则在程序启动的时候打印在控制台上,复制下来即可进行登陆。登陆成功后即可成功访问。
认证
思路分析:我们登陆的账户和密码应该是从数据库进行校验,所以我们在用户进行登陆的时候,应该去数据库进行查询是否有这个用户,如果有对应用户信息,则用户登陆成功,反之登陆失败
具体实现:
1、创建用户表
CREATE TABLE `sys_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `userName` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名', `nickName` VARCHAR(64) NOT NULL DEFAULT 'NUll' COMMENT '昵称', `password` VARCHAR(64) NOT NULL DEFAULT 'NUll' COMMENT '密码', `status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)', `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱', `phoneNumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号', `sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)', `avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像', `userType` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)', `createBy` BIGINT DEFAULT NULL COMMENT '创建人的用户id', `createTime` DATETIME DEFAULT NULL COMMENT '创建时间', `updateBy` BIGINT DEFAULT NULL COMMENT '更新人', `updateTime` DATETIME DEFAULT NULL COMMENT '更新时间', `delFlag` INT DEFAULT '0' COMMENT '删除标志(0代表已删除,1代表未删除)', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
2、引入mysql和mybatisPlus依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency>
3、编写yml文件
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root
4、编写user实体类
/** * 用户表实体类 * @author: QiJingJing * @create: 2022/3/29 */ @Data @AllArgsConstructor @NoArgsConstructor @TableName("sys_user") public class User implements Serializable { /** * 主键 */ @TableId("id") private Long id; /** * 用户名 */ @TableField("userName") private String username; /** * 昵称 */ @TableField("nickName") private String nickName; /** * 密码 */ @TableField("password") private String password; /** * 账号状态(0 正常 1停用) */ @TableField("status") private String status; /** * 邮箱 */ @TableField("email") private String email; /** * 手机号 */ @TableField("phoneNumber") private String phoneNumber; /** * 用户性别(0男,1女,2未知) */ @TableField("sex") private String sex; /** * 头像 */ @TableField("avatar") private String avatar; /** * 用户类型(0管理员,1普通用户) */ @TableField("userType") private String userType; /** * 创建人的用户id */ @TableField("createBy") private Long createBy; /** * 创建时间 */ @TableField("createTime") private Date createTime; /** * 更新人 */ @TableField("updateBy") private Long updateBy; /** * 更新时间 */ @TableField("updateTime") private Date updateTime; /** * 删除标志(0代表未删除,1代表已删除) */ @TableField("delFlag") private Integer delFlag; }
5、编写mapper层代码
@Mapper public interface UserMapper extends BaseMapper<User> { }
6、user表中随便添加一条数据,在测试类中测试是否可以正常查询
@SpringBootTest class Springsecurity1ApplicationTests { @Autowired UserMapper userMapper; @Test void contextLoads() { userMapper.selectList(null).forEach(System.out::println); } }
运行结果:
User(id=1, username=admin, nickName=随风, password=admin, status=0, email=2811157481@qq.com, phoneNumber=17303773603, sex=0, avatar=null, userType=1, createBy=null, createTime=null, updateBy=null, updateTime=null, delFlag=0)
到这一步,说明我们的mysql相关代码没有错误。
7、定义一个UserDetailsServiceImpl
类去实现UserDetailsService
接口,这样我们自己写的类就可以从数据库中查询用户名和密码
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername,username); User user = userMapper.selectOne(queryWrapper); // 如果没有用户,就抛出异常 if(Objects.isNull(user)){ throw new RuntimeException("用户名或者密码错误"); } // todo 查询用户对应的权限信息(等到权限部分进行补充) // 把数据封装为UserDetails返回 return new UserDetailsImpl(user);; } }
上面类需要返回一个UserDetails
对象,我们可以创建一个它的实现类UserDetailsImpl
,将User对象进行封装
@Data @NoArgsConstructor @AllArgsConstructor public class UserDetailsImpl implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { // todo 这里权限部分后面进行补充,先返回null即可 return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
运行程序进行登陆测试,我们会发现,控制台爆出这样的错误
There is no PasswordEncoder mapped for the id “null”
这是因为默认使用PasswordEncoder要求的数据库中密码格式为{id}password,如果找不到id,就会显示null,一般不会采用这样方式,所以我们需要替换掉PasswordEncoder。可以使用SpringSecurity为我们提供的CryptPasswordEncoder
定义一个配置类继承WebSecurityConfigurerAdapter
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 创建BCryptPasswordEncoder 注入容器 */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
在测试类里获取你想设置的密码加密后的样子,比如我的密码是admin
@SpringBootTest class Springsecurity1ApplicationTests { @Autowired PasswordEncoder passwordEncoder; @Test void contextLoads() { String admin = passwordEncoder.encode("admin"); System.out.println(admin); } }
打印结果:
$2a$10$pEluzoEZyQ7AH3Wzp9iLQeg7.Cm/ghFaiLc61bQJU90BvOS3pA6te
存储到对应数据库即可,再次进行登陆密码就是admin即可成功登陆。
8、当然我们也可以自定义登陆接口进行登陆,登陆成功可以利用jwt返回一个token给前端,并且把用户信息存储到redis里面
- 引入一些所需依赖
<!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--fastjson的依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency> <!--jwt的依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
- 导入一些所用的工具类
/** * Redis使用FastJson序列化 * @author: QiJingJing * @create: 2022/3/29 */ public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private Class<T> clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class<T> clazz){ super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0 ){ return null; } String str = new String(bytes,DEFAULT_CHARSET); return JSON.parseObject(str,clazz); } protected JavaType getJavaType(Class<?> clazz){ return TypeFactory.defaultInstance().constructType(clazz); } }
/** * Jwt工具类 * @author: QiJingJing * @create: 2022/3/29 */ public class JwtUtil { // 有效期为一个小时 public static final Long JWT_TTL = 60*60*1000L; // 设置密钥明文 public static final String JWT_KEY = "lili"; public static String getUuid(){ return UUID.randomUUID().toString().replaceAll("-",""); } /** * 生成jwt * @param subject: token中要存放的数据(Json格式) * @return: java.lang.String **/ public static String createJwt(String subject){ // 设置过期时间 JwtBuilder builder = getJwtBuilder(subject,null,getUuid()); return builder.compact(); } /** * 生成jwt * @param subject: token中要存放的数据(Json)格式 * @param ttlMillis:token超时时间 * @return: java.lang.String **/ public static String createJwt(String subject,Long ttlMillis){ // 设置过期时间 JwtBuilder builder = getJwtBuilder(subject,ttlMillis,getUuid()); return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject,Long ttlMillis,String uuid){ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalkey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (ttlMillis == null) { ttlMillis = JwtUtil.JWT_TTL; } Date expDate = new Date(nowMillis + ttlMillis); return Jwts.builder() // 唯一Id .setId(uuid) // 主体,可以是JSON数据 .setSubject(subject) // 签发者 .setIssuer("ll") //签发时间 .setIssuedAt(now) // 使用HS256对称加密算法签名,第二个参数为密钥 .signWith(signatureAlgorithm,secretKey) .setExpiration(expDate); } /** * 创建token * @param id: * @param subject: * @param ttlMillis: * @return: **/ public static String createJwt(String id,String subject, Long ttlMillis){ return getJwtBuilder(subject,ttlMillis,id).compact(); } public static void main(String[] args)throws Exception{ } /** * 生成加密后的密钥 secretkey **/ public static SecretKey generalkey(){ byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); return new SecretKeySpec(encodedKey,0,encodedKey.length,"AES"); } /** * 解析 jwt */ public static Claims parseJwt(String jwt) throws Exception{ SecretKey secretKey = generalkey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
/** * @author: QiJingJing * @create: 2022/3/29 */ @SuppressWarnings(value = {"unchecked", "rawtypes"}) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本对象,Integer,String,实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本对象,Integer,String,实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key redis键 * @param timeout 超时时间 * @return true=设置成功; false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return ture=成功,false = 失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit)); } /** * 获得缓存的基本对象 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return Boolean.TRUE.equals(redisTemplate.delete(key)); } /** * 删除集合对象 * * @param collection 多个对象 */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * @param key 缓存的键值 * @return: 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key){ return redisTemplate.opsForList().range(key,0,-1); } /** * 缓存Set * @param key 缓存键值 * @param dataSet 缓存的数据 * @return: 缓存数据的对象 */ public <T>BoundSetOperations<String,T> setCacheSet(final String key, final Set<T> dataSet){ BoundSetOperations<String,T> setOperations = redisTemplate.boundSetOps(key); for (T t : dataSet) { setOperations.add(t); } return setOperations; } /** * 获得缓存的set * @param key */ public <T> Set<T> getCacheSet(final String key){ return redisTemplate.opsForSet().members(key); } /** * 缓存Map * @param key * @param dataMap */ public <T> void setCacheMap(final String key,final Map<String,T> dataMap){ if (dataMap != null){ redisTemplate.opsForHash().putAll(key,dataMap); } } /** * 获得缓存的Map * @param key * @return: */ public <T> Map<String,T> getCacheMap(final String key){ return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key,String hKey,final T value){ redisTemplate.opsForHash().put(key,hKey,value); } /** * 获取Hash中的数据 * @param key redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key,final String hKey){ HashOperations<String,String,T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key,hKey); } /** * 获取多个Hash中的数据 * @param key Redis键集合 * @param hKeys Hash键集合 * @return: Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key,final Collection<Object> hKeys){ return redisTemplate.opsForHash().multiGet(key,hKeys); } /** * 获得缓存的基本对象列表 * @param pattern 字符串前缀 * @return: 对象列表 */ public Collection<String> keys(final String pattern){ return redisTemplate.keys(pattern); } }
/** * * @author: QiJingJing * @create: 2022/3/29 */ @Configuration public class RedisConfig { @Bean @SuppressWarnings(value = {"unchecked","rawtypes"}) public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){ RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }
/** * 响应结果集 * @author: QiJingJing * @create: 2022/3/29 */ @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult <T>{ /** * 状态码 */ private Integer code; /** * 提示信息 */ private String msg; /** * 查询到的结果数据 */ private T data; public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } }
/** * @author: QiJingJing * @create: 2022/3/29 */ public class WebUtils { /** * 将字符串渲染到客户端 * @param response 渲染对象 * @param string 待渲染的字符串 */ public static void renderString(HttpServletResponse response, String string){ try{ response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); }catch (IOException e){ e.printStackTrace(); } } }
自定义登陆接口需要对这个接口进行放行,也就是不用登陆也能访问这个接口,在接口中我们需要通过AuthenticationManager
的authenticate方法来认证,所以需要在SecurityConfig中配置把AuthenticationManager
注入容器,并且配置需要放行的接口("/user/login"),因此在SecurityConfig
配置类中需要新添加的代码如下:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 把认证AuthenticationManager注入bean容器 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf .csrf().disable() // 不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登陆接口 允许匿名访问(登陆后则不能访问) .antMatchers("/user/login").anonymous() // 登陆与否都可以访问 //.antMatchers("/hello").permitAll() // 除了上面所有的请求全部需要鉴权认证 .anyRequest().authenticated(); } }
编写登陆接口的service层以及Controller层
public interface LoginService { ResponseResult<Object> login(User user); }
@Service public class LoginServiceImpl implements LoginService { @Autowired RedisCache redisCache; @Autowired AuthenticationManager authenticationManager; @Override public ResponseResult<Object> login(User user) { // 获取认证 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); Authentication authenticate = authenticationManager.authenticate(token); // 认证没通过给出提示 if (Objects.isNull(authenticate)) { throw new RuntimeException("登陆失败"); } //认证通过后,使用userId生成一个jwt,存入ResponseResult返回 UserDetailsImpl userDetails = (UserDetailsImpl) authenticate.getPrincipal(); String id = userDetails.getUser().getId().toString(); String jwt = JwtUtil.createJwt(id); HashMap<String, String> map = new HashMap<>(); map.put("token", jwt); // 把完整的信息存入redis,userId作为主键 redisCache.setCacheObject("login"+id,userDetails); return new ResponseResult<>(200, "登陆成功", map); } }
@RestController public class LoginController { @Autowired private LoginService loginService; @PostMapping("/user/login") public ResponseResult<Object>login(@RequestBody User user){ return loginService.login(user); } }
PostMan进行登陆测试
恭喜你,登陆成功
温馨提示:记得打开redis哦,不然会报错,因为我用到了redis