一、准备一个spring boot项目
1、引入基础依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.13</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.1</version></dependency></dependencies>
2、项目配置文件,连接数据库
server:port: 80servlet:context-path: /spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghaiusername: rootpassword: zzybzb
3、一个简单接口
package com.pzz.controller;import com.pzz.mapper.XcUserMapper;
import com.pzz.po.XcUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@Slf4j
@RestController
public class LoginController {@AutowiredXcUserMapper userMapper;@RequestMapping("/login-success")public String loginSuccess() {return "登录成功";}@RequestMapping("/r/r1")public String r1() {return "访问r1资源";}@RequestMapping("/r/r2")public String r2() {return "访问r2资源";}}
4、测试接口能跑
二、整合spring security
1、引入依赖配置
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId><version>2.1.3.RELEASE</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.1.3.RELEASE</version></dependency>
2、启动,再次访问需要登录
这里的admin无法登录
3、添加配置文件
- 用户信息
在内存配置两个用户:zhangsan、lisi
zhangsan用户拥有的权限为p1
lisi用户拥有的权限为p2 - 密码方式
暂时采用明文方式 - 安全拦截机制
/r/**开头的请求需要认证
package com.pzz.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;/*** @description 安全管理配置*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//配置用户信息服务@Beanpublic UserDetailsService userDetailsService() {//这里配置用户信息,这里暂时使用这种方式将用户存储在内存中InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();//在内存创建zhangsan,密码123,分配权限p1manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());//在内存创建lisi,密码456,分配权限p2manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());return manager;}@Beanpublic PasswordEncoder passwordEncoder() {
// //密码为明文方式return NoOpPasswordEncoder.getInstance();
// return new BCryptPasswordEncoder();}//配置安全拦截机制@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过.anyRequest().permitAll()//其它请求全部放行.and().formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-successhttp.logout().logoutUrl("/logout");//退出地址}}
4、访问测试
- 只拦截/r/**路径下的请求,所以login-success没问题
- 访问/r/r1需要登录
- 输入配置文件设置的用户名密码登录
- 再次访问,不需要登录
5、测试退出
- 配置文件配置了退出的路径
- 退出登录,并再次请求/r/r1
6、授权测试
用户认证通过去访问系统资源时spring security进行授权控制,判断用户是否有该资源的访问权限,如果有则继续访问,如果没有则拒绝访问。
6.1、配置用户拥有哪些权限
张三的权限是p1
李四的权限是p2
6.2、指定资源与权限的关系
@RequestMapping("/r/r1")@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问public String r1() {return "访问r1资源";}@RequestMapping("/r/r2")@PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问public String r2() {return "访问r2资源";}
6.3、重启,访问测试
- 张三登录,分别访问/r/r1和/r/r2
- lisi登录,分别访问/r/r1和/r/r2
7、工作原理
7.1、工作原理
pring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。
spring Security功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
- SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。
在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext; - UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变; - FilterSecurityInterceptor
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了; - ExceptionTranslationFilter
能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
7.2、执行流程
Spring Security的执行流程如下:
- 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
- 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证。
- 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
- SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
- 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
三、连数据库
1、实体类信息
@Data
@TableName("xc_user")
public class XcUser implements Serializable {private static final long serialVersionUID = 1L;private String id;private String userName;private String password;private String salt;private String name;private String nickname;private String wxUnionid;private String companyId;/*** 头像*/private String userpic;private String utype;private Timestamp birthday;private String sex;private String email;private String cellphone;private String qq;/*** 用户状态*/private String status;private Timestamp createTime;private Timestamp updateTime;}
2、数据库表
DROP TABLE IF EXISTS `xc_user`;
CREATE TABLE `xc_user` (`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`user_name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`password` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`salt` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`wx_unionid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信unionid',`nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',`name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`userpic` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',`company_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`utype` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`birthday` datetime NULL DEFAULT NULL,`sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`email` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`cellphone` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`qq` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`status` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户状态',`create_time` datetime NOT NULL,`update_time` datetime NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `unique_user_username`(`user_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
3、mapper
@Mapper
public interface XcUserMapper extends BaseMapper<XcUser> {}
4、实现UserDetailsService接口,重写loadUserByUsername方法
package com.pzz.service;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.pzz.domain.LoginUser;
import com.pzz.mapper.XcUserMapper;
import com.pzz.po.XcUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.util.ObjectUtils;/*** 用户验证处理** @author ruoyi*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);@Autowiredprivate XcUserMapper xcUserMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper<XcUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(XcUser::getUserName, username);XcUser xcUsers = xcUserMapper.selectOne(lambdaQueryWrapper);//如果沒有用戶就拋出异常if (ObjectUtils.isEmpty(xcUsers)) {throw new RuntimeException("用户名或密码错误");}//查到用户,返回信息return new LoginUser(xcUsers);}
}
5、封装返回的UserDetails
@Data
public class LoginUser implements UserDetails
{private static final long serialVersionUID = 1L;/*** 用户ID*/private Long userId;/*** 部门ID*/private Long deptId;/*** 用户唯一标识*/private String token;/*** 登录时间*/private Long loginTime;/*** 过期时间*/private Long expireTime;/*** 登录IP地址*/private String ipaddr;/*** 登录地点*/private String loginLocation;/*** 浏览器类型*/private String browser;/*** 操作系统*/private String os;/*** 权限列表*/private Set<String> permissions;/*** 用户信息*/private XcUser user;public LoginUser(){}public LoginUser(XcUser user){this.user = user;}public LoginUser(XcUser user, Set<String> permissions){this.user = user;this.permissions = permissions;}public LoginUser(Long userId, Long deptId, XcUser user, Set<String> permissions){this.userId = userId;this.deptId = deptId;this.user = user;this.permissions = permissions;}@Overridepublic String getPassword(){return user.getPassword();}@Overridepublic String getUsername(){return user.getUserName();}/*** 账户是否未过期,过期无法验证*/@Overridepublic boolean isAccountNonExpired(){return true;}/*** 指定用户是否解锁,锁定的用户无法进行身份验证* * @return*/@Overridepublic boolean isAccountNonLocked(){return true;}/*** 指示是否已过期的用户的凭据(密码),过期的凭据防止认证* * @return*/@Overridepublic boolean isCredentialsNonExpired(){return true;}/*** 是否可用 ,禁用的用户不能身份验证* * @return*/@Overridepublic boolean isEnabled(){return true;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities(){return null;}
}
6、明文登录
6.1、设置明文
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {//密码为明文方式return NoOpPasswordEncoder.getInstance();
// return new BCryptPasswordEncoder();}
}
6.2、测试
7、密码加密登录
7.1、设置密码加密
@Beanpublic PasswordEncoder passwordEncoder() {
// //密码为明文方式
// return NoOpPasswordEncoder.getInstance();return new BCryptPasswordEncoder();}
7.2、生成密码,存入数据库
7.3、测试,登录成功
8、注意,把自定义的用户名密码去掉
四、整合jwt,登录成功返回用户信息
1、引入依赖
<!-- 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.5.1</version></dependency>
2、JwtUtils工具类
package com.pzz.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;public class JwtUtils {private static final String SECRET_KEY = "yourSecretKey";private static final long EXPIRATION_TIME = 86400000; // 过期时间为1天/*** 用于生成JWT。* 它接收一个用户ID作为参数,并使用内置的算法和秘钥生成一个包含用户ID和过期时间的签名。* @param userId* @return*/public static String generateToken(String userId) {Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME);return Jwts.builder().setSubject(userId).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET_KEY).compact();}/*** 用于从JWT中提取用户ID。* 它接收一个JWT令牌作为参数,并解析其中的主题(主体)部分来获取用户ID。* @param token* @return*/public static String getUserIdFromToken(String token) {Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();return claims.getSubject();}/*** 用于验证JWT的有效性。* 它接收一个JWT令牌作为参数,并解析和验证签名。* 如果令牌有效且未过期,则返回true;否则返回false。* @param token* @return*/public static boolean isTokenValid(String token) {try {Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);return true;} catch (Exception e) {return false;}}
}
3、redis 工具类
package com.pzz.utils;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;/*** spring redis 工具类** @author ruoyi**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{@Autowiredpublic 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 true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获取有效时间** @param key Redis键* @return 有效时间*/public long getExpire(final String key){return redisTemplate.getExpire(key);}/*** 判断 key是否存在** @param key 键* @return true 存在 false不存在*/public Boolean hasKey(String key){return redisTemplate.hasKey(key);}/*** 获得缓存的基本对象。** @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 redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public boolean deleteObject(final Collection collection){return redisTemplate.delete(collection) > 0;}/*** 缓存List数据** @param key 缓存的键值* @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> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/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, final 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);}/*** 删除Hash中的某条数据** @param key Redis键* @param hKey Hash键* @return 是否成功*/public boolean deleteCacheMapValue(final String key, final String hKey){return redisTemplate.opsForHash().delete(key, hKey) > 0;}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}
4、redis的配置
spring:redis:host: 127.0.0.1post: 6379
5、创建authenticationManagerBean对象,和配置安全拦截机制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//配置安全拦截机制@Overrideprotected void configure(HttpSecurity http) throws Exception {http //关闭csrf.csrf().disable()//不通过session获取securityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//放行登录接口.antMatchers("/login").anonymous()//除了上面的请求,其他的请求必须授权认证.anyRequest().authenticated();}
}
6、登录的接口
/*** 登录的方法* @return*/@PostMapping("/login")public R loginSuccess(@RequestBody XcUser xcUser) {return loginService.login(xcUser);}
7、登录的实现,使用用户ID生成jwt
@Service
public class LoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;/*** 登录认证的方法* @param xcUser* @return*/@Overridepublic R login(XcUser xcUser) {//Authentication authenticate 进行用户认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(xcUser.getUserName(), xcUser.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);//认证失败,返回异常if (ObjectUtils.isEmpty(authenticate)){throw new RuntimeException("登录失败...");}//认证通过,生成jwt,返回LoginUser loginUser = (LoginUser)authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();//根据用户ID生成jwtString jwt = JwtUtils.generateToken(userId);Map<String,String> map = new HashMap<>();map.put("token",jwt);//把完整的用户信息存入redis,userId作为keyredisCache.setCacheObject("login:"+userId,loginUser);String username = loginUser.getUser().getUserName().toString();return R.ok(map,"登录成功,欢迎:"+username);}
}
8、测试接口
五、认证过滤器
当用户登录后,生成的token信息存入headers中,每次请求应该携带token,作为是否登录的验证
1、认证过滤器JwtAuthenticationTokenFilter
package com.pzz.filter;import com.pzz.domain.LoginUser;
import com.pzz.utils.JwtUtils;
import com.pzz.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Objects;/*** @author pzz* @date 2023/7/26 22:51*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)){//放行filterChain.doFilter(request,response);return;}//解析tokenString userId;try {userId = JwtUtils.getUserIdFromToken(token);} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//从redis中获取用户信息String redisKey = "login:"+userId;LoginUser loginUser = (LoginUser)redisCache.getCacheObject(redisKey);if (Objects.isNull(loginUser)){throw new RuntimeException("用户未登录");}//存入securContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request,response);}
}
2、配置认证过滤器
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {//配置认证过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}
}
3、根据ID查询用户信息的方法
/*** 根据用户ID查询用户信息* @param id* @return*/@RequestMapping("/user/{id}")public R getuser(@PathVariable("id") String id) {XcUser xcUser = userMapper.selectById(id);return R.ok(xcUser);}
4、测试
4.1、未携带token
4.2、携带token
5、退出登录
5.1、controller代码
/*** 注销登录*/@GetMapping("/logout")public R logout(){return loginService.logout();}
5.2、service代码
/*** 退出登录* @return*/@Overridepublic R logout() {//获取SecurityContextHolder中的用户IDUsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser)authenticationToken.getPrincipal();String userId = loginUser.getUser().getId();//删除redis中的值redisCache.deleteObject("login:"+userId);return R.ok(null,"注销成功");}
六、授权
1、相关配置
1.1、开启访问资源所需权限
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {}
1.2、访问路径设置权限
@RequestMapping("/hello")@PreAuthorize("hasAuthority('test')")//拥有test权限方可访问public String hello() {return "hello";}
2、封装权限、给用户添加权限
2.1、封装用户的实体类,用户设置权限
@Data
public class LoginUser implements UserDetails{/*** 权限列表*/private Set<String> permissions;public LoginUser(XcUser user, Set<String> permissions){this.user = user;this.permissions = permissions;}@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities(){if (authorities != null){return authorities;}authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}
}
2.2、用户登录时查询用户的权限,当前设置用户的权限为test
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询用户的权限Set<String> set = new HashSet<>();set.add("test");//查到用户,返回信息return new LoginUser(xcUsers,set);}
}
2.3、过滤器拦截权限
3、测试
3.1、登录后,访问hello
3.2、修改hello的访问权限为test111,测试
4、从数据查用户的权限
权限一般使用五个表
- 用户表
- 角色表
- 用户_角色_中间表
- 菜单表(权限表)
- 角色_权限_中间表
4.1、查询权限的SQL
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.pzz.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String" parameterType="integer">SELECTDISTINCTsm.permsfromsys_user_role surleft join sys_role sr on sur.role_id = sr.role_idLEFT JOIN sys_role_menu srm on sr.role_id = srm.role_idleft join sys_menu sm on srm.menu_id = sm.menu_idwheresm.perms is not nulland sm.perms != ""and sur.user_id = #{userId}</select>
</mapper>
4.2、登录成功时,设置用户的权限
4.3、修改访问hello接口的权限
4.4、访问测试
七、跨域
1、springboot设置跨域(省略)
2、security设置跨域
//设置跨域http.cors();
八、security认证流程
结束!
hy:23
真正的幸福来自于内心的满足和积极的生活态度。