一:初始化项目

1、创建SpringBoot项目

        <!--spring boot--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>

2、整合MyBatis(具体参考我的《Spring Boot整合MyBatis》)

        <!-- mybatis --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency>

3、引入SpringSecurity

        <!--spring security 启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--jwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>

4、创建测试接口http://localhost:8080/hello,进行测试。
当引入security成功之后,在登录测试接口时就会跳转到默认的登录页http://localhost:8080/login。
(默认用户名:user,密码:控制台输出,我这里是28f2036e-c4f2-4097-9a5c-17e7c8a429c4,)

二:简单原理介绍

1、登录校验流程
2、SpringSecurity简单过滤器链(完整的过滤器大概是14个),前后端分离一般用jwt,前后端不分离一般采用session

三:认证(UsernamePasswordAuthenticationFiter)

1、登录流程:

①、登录、自定义登录接口:
调用ProviderManager的方法进行认证,如果认证通过生成jwt和把用户信息存到redis中。

// 认证 实现代码类
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUserName(), sysUser.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 认证通过 生成jwt
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getSysUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);// 使用userId生成 jwt
Map<String, String> map = new HashMap<>(1);
map.put("token", jwt);
// 认证通过 存入 redis
redisCache.setCacheObject("login:" + userId, loginUser);public class SecurityConfig extends WebSecurityConfigurerAdapter {//认证 配置代码@Overrideprotected void configure(HttpSecurity http) throws Exception {// ......}
}

②、登录、自定义UserDetailsService:
实现到db中查询用户信息,因为原接口是从内存中查询的。

//重写 UserDetailsService 的 loadUserByUsername 方法
public class UserDetailsServiceImpl implements UserDetailsService{@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//......}
}//重写UserDetails返回的用户信息
public class LoginUser implements UserDetails {//......
}

2、token认证:

①、校验、定义jwt认证过滤器:
获取toekn、解析token获取其中的userId、从redis中获取用户信息、使用SecurityContextHolder.getContext().setAuthentication()方法存储该对象,这样其他过滤器会通过SecurityContextHolder来获取当前用户信息。

// token认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 省略......  拦截到 token不合法等情况 // 将 Authentication对象存入 SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}

四:授权(FilterSecurityInterceptor)

1、将用户的权限信息封装到Authentcation当中,存到SecurityContextHolder中。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 省略 ......// 根据用户id查询权限字符串集合List<String> list = sysMenuDao.selectPermsByUserId(userInfo.getId());return new LoginUser(userInfo, list);}
}public class LoginUser implements UserDetails {// ......@JSONField(serialize = false) //fastjson注解,表示此属性不会被序列化到redis当中private List<SimpleGrantedAuthority> authorities;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 权限为空的时候才往遍历,不为空直接返回if (authorities != null) {return authorities;}//把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象authorities = new ArrayList<>();for (String permission : permissions) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);authorities.add(authority);}return authorities;}// ......
}// token过滤器中( JwtAuthenticationTokenFilter )
// 将Authentication对象(用户信息、已认证状态、权限信息)存入 SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

2、从SecurityContextHolder中获取Authentcation对象,再获取其中的权限信息。

// token过滤器中( JwtAuthenticationTokenFilter )
String redisKey = "login:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);// 从redis中获取用户信息

3、设置每个资源(方法)所需要的权限(注解形式设置权限)
RBAC(基于角色的权限控制)

//SecurityConfig 配置类开启权限注解功能
@EnableGlobalMethodSecurity(prePostEnabled = true)/***** 对应资源(方法)上授权 *****/
//常用,该用户在数据库中有 system:dept:list 这个权限标识才能访问
@PreAuthorize("hasAuthority('system:dept:list')")
@PreAuthorize("hasAnyAuthority()")
@PreAuthorize("hasAnyRole()")
@PreAuthorize("hasAnyRole()")
@PreAuthorize("hasPermission()")

五:失败处理(ExceptionTranslationFilter)

public class SecurityConfig extends WebSecurityConfigurerAdapter {// ......@Overrideprotected void configure(HttpSecurity http) throws Exception {http.exceptionHandling() // 配置异常处理器.authenticationEntryPoint(authenticationEntryPoint)// 认证失败.accessDeniedHandler(accessDeniedHandler); // 授权失败}// ......
}

1、认证失败处理器

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {// ......
}

2、授权失败处理器

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {// ......
}

六:跨域

同时处理 springboot跨域 和 springsecurity跨域

// springboot 跨域配置类
@Configuration
public class CorsConfig implements WebMvcConfigurer {// ......
}// SecurityConfig配置类
http.cors();// spring security 允许跨域

七:完整代码(可以直接使用)

技术栈说明:
根据三更草堂课程,微调技术框架。
springboot 2.6.6 、 jdk1.8 、 mybatis1.3.2 、redis、SpringSecurity、jwt

①POM文件

<?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.6.6</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>boot_security</artifactId><version>0.0.1-SNAPSHOT</version><name>boot_security</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><!--spring boot--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></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><!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--jwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><!-- mybatis --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency></dependencies><build><!--mybatis加载配置文件--><resources><resource><directory>src/main/resources</directory></resource><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource></resources><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

②application.yml文件

server:port: 8080spring:# 数据源配置datasource:url: jdbc:mysql://localhost:3306/sg_security?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver# redis配置redis:host: localhostport: 6379password: 123456# mybatis配置
mybatis:# 配置SQL映射文件路径# mapper-locations: classpath:/mapper/*.xmlmapper-locations: classpath*:com/example/boot_security/**/mapper/*.xml# 驼峰命名configuration:mapUnderscoreToCamelCase: true

③config包的10个配置文件

1.授权失败

/*** 授权失败*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult result = new ResponseResult(403, "您的权限不足!");String json = JSON.toJSONString(result);// 将字符串渲染到客户端WebUtils.renderString(response, json);}
}

2、认证失败

/*** 认证失败*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult result = new ResponseResult(401, "认证失败,请重新登录!");String json = JSON.toJSONString(result);// 将字符串渲染到客户端WebUtils.renderString(response, json);}
}

3、springboot 跨域配置类

/*** springboot 跨域配置类*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}

4、Redis使用FastJson序列化

/*** Redis使用FastJson序列化*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static {ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic 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);}
}

5、token认证过滤器

/*** token认证过滤器** 2022/1/5-14:12* 作用:解析请求头中的token。并验证合法性* 继承 OncePerRequestFilter 保证请求经过过滤器一次*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");// 没有tokenif (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);//放行,因为后面的会抛出相应的异常return;}// 非法tokenString userId;try {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("非法token!");}String redisKey = "login:" + userId;LoginUser loginUser = redisCache.getCacheObject(redisKey);// 从redis中获取用户信息// redis中用户不存在if (Objects.isNull(loginUser)) {throw new RuntimeException("redis中用户不存在!");}// 将Authentication对象(用户信息、已认证状态、权限信息)存入 SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}

6、Jwt工具类

/*** Jwt工具类* <p>* 2022/1/4-22:16*/
public class JwtUtil {/*** 有效期为* 60 * 60 *1000  一个小时*/public static final Long JWT_TTL = 60 * 60 * 1000L;/*** 设置秘钥明文*/public static final String JWT_KEY = "sangeng";public static String getUUID() {String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成 jtw** @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject) {// 设置过期时间 空JwtBuilder builder = getJwtBuilder(subject, null, getUUID());return builder.compact();}/*** 生成 jtw** @param subject   token中要存放的数据(json格式)* @param ttlMillis token超时时间* @return*/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;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder()// 唯一的ID.setId(uuid)// 主题  可以是JSON数据.setSubject(subject)// 签发者.setIssuer("sg")// 签发时间.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) {// 设置过期时间JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);return builder.compact();}/*** 生成加密后的秘钥 secretKey** @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}//测试方法public static void main(String[] args) throws Exception {//JWT加密String jwt = createJWT("123456");System.out.println(jwt);//eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhYzBlYzk3ZDM0OGI0YmVkYjlmY2Q5NmZiNGViMmZkNCIsInN1YiI6IjEyMzQ1NiIsImlzcyI6InNnIiwiaWF0IjoxNjQ4OTg2NjkxLCJleHAiOjE2NDg5OTAyOTF9.G-K2XlcmE2lP7EOldbpp1rs743uvTu1NoYMo_g7sjkQ//JWT解密  时间过期会报错,须重新生成再解析Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJmYTI3NTBjMzk0OGU0Mjc1YjcxNTI5OTdkODMxOWExNyIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0OTAzODc2OSwiZXhwIjoxNjQ5MDQyMzY5fQ.tcDLOBpTPmYcYhKx1R-gziD9s9viwtvaJ10xiJO0vAs");String subject = claims.getSubject();System.out.println(subject);}
}

7、redis工具类 进一步封装 RedisTemplate

/*** redis工具类 进一步封装 RedisTemplate*/
@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 缓存键值* @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 long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** 缓存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* @param hkey*/public void delCacheMapValue(final String key, final String hkey) {HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(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);}
}

8、redis配置类

/*** redis配置类* 避免存入redis中的key看上去乱码的现象*/
@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;}
}

9、SpringSecurity 核心配置类

/*** SpringSecurity 核心配置类* prePostEnabled = true 开启注解权限认证功能*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启@PreAuthorize()注解权限功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {//认证过滤器@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;// 认证失败处理器@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;// 授权失败处理器@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;/*** 密码机密处理器* <p>* 将BCryptPasswordEncoder对象注入到spring容器中,更换掉原来的 PasswordEncoder加密方式* 原PasswordEncoder密码格式为:{id}password。它会根据id去判断密码的加密方式。* 如果没替换原来的加密方式,数据库中想用明文密码做测试,将密码字段改为{noop}123456这样的格式*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证配置* anonymous():匿名访问:未登录可以访问,已登录不能访问* permitAll():有没有认证都能访问:登录或未登录都能访问* denyAll(): 拒绝* authenticated():认证之后才能访问* hasAuthority():包含权限*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http// 关闭csrf(前后端分离项目要关闭此功能).csrf().disable()// 禁用session (前后端分离项目,不通过Session获取SecurityContext).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 请求认证配置.authorizeRequests()// 允许匿名访问:未登录可以访问,已登录不能访问.antMatchers("/login").anonymous()// .antMatchers("/login").permitAll()// 登录或未登录都能访问// .antMatchers("/textMybatis").hasAuthority("system:dept:list22")// 任意用户,认证之后才可以访问(除上面外的).anyRequest().authenticated();// 添加token过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 配置异常处理器http.exceptionHandling()// 认证失败.authenticationEntryPoint(authenticationEntryPoint)// 授权失败.accessDeniedHandler(accessDeniedHandler);// spring security 允许跨域http.cors();}/*** 注入AuthenticationManager 进行用户认证*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}

10、WebUtils

/*** 将字符串渲染到客户端* 向响应之中写入中聚类** @author bing_  @create 2022/1/4-22:20*/
public class WebUtils {/*** 将字符串渲染到客户端** @param response 渲染对象* @param string   待渲染的字符串* @return null*/public static String 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();}return null;}
}

④hello包(测试的,有没有都行)

/*** 测试接口 账户名:sg  密码:1111*/
@RestController
@RequestMapping("/test")
public class HelloController {@Autowiredprivate SysUserDao sysUserDao;/*** 测试 springBoot*/@GetMapping("/SpringBoot")@PreAuthorize("hasAuthority('system:dept:list')")//授权public String testSpringBoot(){return "测试 springBoot";}/*** 测试 mybatis*/@GetMapping("/Mybatis")public SysUser textMybatis(){return sysUserDao.getUserInfo("sg");}
}

⑤sysSecurity包

1、controller包(1个)

/*** @Author jws* @Date 2022-04-03 20:13** 一个坑:退出路径如果只有 "/logout" 会报403 ,原因不明* 解决方法:前面添加个前路径,或者换一个名就可以了*/
@RestController
public class LoginController {@Autowiredprivate LoginService loginService;/*** 登录*/@PostMapping("/login")public ResponseResult login(@RequestBody SysUser sysUser) {return loginService.login(sysUser);}/*** 退出登录*/@GetMapping("/user/logout")public ResponseResult logout() {return loginService.logout();}
}

2、dao包(2个)

1、SysMenuDao 接口

public interface SysMenuDao {/*** 根据用户id查询权限字符串集合*/List<String> selectPermsByUserId(Long userId);
}

2、SysUserDao 接口

public interface SysUserDao {/*** 获取用户信息*/@Select("select * from sys_user where user_name = #{username}")SysUser getUserInfo(String username);
}

3、entity包(4个)

1、LoginUser 类

/*** 重写UserDetails返回的用户信息* SpringSecurity返回的用户信息实体类*/
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {private SysUser sysUser;//用户信息private List<String> permissions;//权限信息public LoginUser(SysUser sysUser, List<String> permissions) {this.sysUser = sysUser;this.permissions = permissions;}@JSONField(serialize = false) //fastjson注解,表示此属性不会被序列化,因为SimpleGrantedAuthority这个类型不能在redis中序列化private List<SimpleGrantedAuthority> authorities;/*** 获取权限信息*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 权限为空的时候才往遍历,不为空直接返回if (authorities != null) {return authorities;}//把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象authorities = new ArrayList<>();for (String permission : permissions) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);authorities.add(authority);}return authorities;}/*** 获取密码*/@Overridepublic String getPassword() {return sysUser.getPassword();}/*** 获取用户名*/@Overridepublic String getUsername() {return sysUser.getUserName();}/*** 判断是否过期*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 是否锁定*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 是否没有超时*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否可用*/@Overridepublic boolean isEnabled() {return true;}
}

2、ResponseResult类

/*** 统一返回结果** @author bing_  @create 2022/1/4-22:13*/
@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 Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}
}

3、SysMenu类

/*** 菜单表(sys_menu)实体表*/
@Data
public class SysMenu{private Long id;private String menuName;//菜单名private String path;//路由地址private String component;//组件路径private String visible;//菜单状态(0显示 1隐藏)private String status;//菜单状态(0正常 1停用)private String perms;//权限标识private String icon;//菜单图标private Long createBy;private LocalDateTime createTime;private Long updateBy;private LocalDateTime updateTime;private Integer delFlag;//是否删除(0未删除 1已删除)private String remark;//备注
}

4、SysUser 类

/*** 用户表(sys_user)实体表*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser {private Long id;//主键private String userName;//用户名private String nickName;//昵称private String password;//密码private String status;//账号状态(0正常 1停用)private String email;//邮箱private String phonenumber;//手机号private String sex;//用户性别(0男,1女,2未知)private String avatar;//头像private String userType;//用户类型(0管理员,1普通用户)private Long createBy;//创建人的用户idprivate LocalDateTime createTime;//创建时间private Long updateBy;//更新人private LocalDateTime updateTime;//更新时间private Integer delFlag;//删除标志(0代表未删除,1代表已删除)
}

4、mapper包(1个)

SysMenuMapper.xml文件

<?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.example.boot_security.sysSecurity.dao.SysMenuDao"><!--根据用户id查询权限字符串集合--><select id="selectPermsByUserId" resultType="java.lang.String">SELECT DISTINCT m.permsFROM sys_user_role urLEFT JOIN sys_role r ON ur.role_id = r.idLEFT JOIN sys_role_menu rm ON r.id = rm.role_idLEFT JOIN sys_menu m ON rm.menu_id = m.idWHERE ur.user_id = #{userId}</select>
</mapper>

5、service包(2个接口,3个实现)

1、LoginService 接口

/*** @Author jws* @Date 2022-04-03 20:13*/
public interface LoginService {/*** 登录*/ResponseResult login(SysUser sysUser);/*** 退出登录*/ResponseResult logout();
}

2、SysUserService 接口

public interface SysUserService {/*** 根据用户名获取用户信息*/SysUser getUserInfo(String username);
}

3、impl包(实现)

1、LoginServiceImpl
/*** @Author jws* @Date 2022-04-03 20:13*/
@Service
public class LoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(SysUser sysUser) {// 认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUserName(), sysUser.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 认证没通过if (Objects.isNull(authenticate)) {throw new RuntimeException("用户名或密码错误!");}// 认证通过 生成jwtLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getSysUser().getId().toString();String jwt = JwtUtil.createJWT(userId);Map<String, String> map = new HashMap<>(1);map.put("token", jwt);// 认证通过 存入 redisredisCache.setCacheObject("login:" + userId, loginUser);return new ResponseResult(200, "登录成功", map);}@Overridepublic ResponseResult logout() {LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();Long userId = loginUser.getSysUser().getId();// 清空redisredisCache.deleteObject("login:" + userId);return new ResponseResult(200, "退出成功");}
}
2、SysUserServiceImpl
@Service
public class SysUserServiceImpl implements SysUserService {@Autowiredprivate SysUserDao systemUserDao;/*** 根据用户名获取用户信息*/@Overridepublic SysUser getUserInfo(String username) {return systemUserDao.getUserInfo(username);}
}
3、UserDetailsServiceImpl
/*** 重写框架的 UserDetailsService 的 loadUserByUsername 方法* 从数据库中查询用户信息* 因为原框架的内用信息是存在内存中的*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate SysMenuDao sysMenuDao;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser userInfo = sysUserService.getUserInfo(username);if (Objects.isNull(userInfo)) {throw new RuntimeException("用户名和密码错误!");}List<String> list = sysMenuDao.selectPermsByUserId(userInfo.getId());// 把数据封装成 UserDetails 返回,参数:用户信息、权限列表return new LoginUser(userInfo, list);}
}

⑥启动类

@SpringBootApplication
@MapperScan("com/example/boot_security/**/dao")
public class BootSecurityApplication {public static void main(String[] args) {SpringApplication.run(BootSecurityApplication.class, args);}
}

最后这个项目目录,以上是全部代码,可以运行的。

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

SpringBoot_整合SpringSecurity(前后端分离版)相关推荐

  1. echarts前后端交互数据_SpringBoot2.0实战(26)整合SpringSecurity前后端分离JSON交互...

    在前端的文章中,我们实现了使用 SpringSecurity 实现登录鉴权,并使用数据库存储用户信息,实现登录鉴权 SpringBoot2.0实战(24)整合SpringSecurity之最简登录方法 ...

  2. 使用SpringBoot + Vue (若依前后端分离版) 写项目的一些总结(持续更新...)

    使用SpringBoot + Vue(若依前后端分离版) 写项目的一些总结 获取Redis服务 @Autowired private RedisCache redisCache; String cap ...

  3. 若依前后端分离版怎样去掉登录验证码

    场景 若依前后端分离版手把手教你本地搭建环境并运行项目: 若依前后端分离版手把手教你本地搭建环境并运行项目_BADAO_LIUMANG_QIZHI的博客-CSDN博客_若依前后端分离版本的配置 上面在 ...

  4. 若依前后端分离版手把手教你本地搭建环境并运行项目

    场景 RuoYi-Vue是一款基于SpringBoot+Vue的前后端分离极速后台开发框架. RuoYi 官网地址:http://ruoyi.vip RuoYi 在线文档:http://doc.ruo ...

  5. 若依前后端分离版数据库已经存在的字典添加一条后刷新没作用,必须清除Redis缓存

    场景 使用若依的前后端分离版,前端下拉框的使用直接查询的是字典表中的数据. 对于某个类型的字典如果之前已经添加过并使用过,后来想要再添加一条此类型的字典. 在数据库中添加后,前端刷新下,发现没有获取到 ...

  6. 若依前后端分离版怎样根据数据库生成代码并快速实现某业务的增删改查

    场景 使用若依的前后端分离版,怎样使用其代码生成实现对单表的增删改查导出的业务. 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程 ...

  7. 若依前后端分离版怎样修改主页面和浏览器上的图标和标题

    场景 使用若依的前后端分离版,,其默认的图标和标题等如下 如果想要修改为自己想要的标题和图标,实现类似下面的效果 注: 博客: https://blog.csdn.net/badao_liumang_ ...

  8. 若依管理系统前后端分离版基于ElementUI和SpringBoot怎样实现Excel导入和导出

    场景 使用若依前后端分离版实现Excel的导入和导出. 前端:Vue+ElementUI 后端:SpringBoot+POI+Mysql 注: 博客: https://blog.csdn.net/ba ...

  9. 若依前后端分离版怎样修改主页面显示请求的SpringBoot后台数据

    场景 使用若依的前后端分离版,本来的首页效果是 现在如果要根据具体业务实现从后台获取要显示的数据实现类似下面的效果 注: 博客: https://blog.csdn.net/badao_liumang ...

  10. Spring Boot与JWT整合实现前后端分离的用户认证

    前言 本篇使用java-jwt作为JWT库,与Spring Boot整合实现前后端分离架构中用户认证. Spring Boot项目搭建参考: [Spring Boot系列]1. 项目搭建之一 关于JW ...

最新文章

  1. Android之本地数据存储(一):SharedPreferences
  2. C#中三种定时器对象的比较
  3. 电子计算机系统可以分为几类,电子计算机分为两大类.doc
  4. Java程序员的典型工作过程有哪些_Java程序员都要经历哪些阶段
  5. toj 4613 Number of Battlefields
  6. sphinx文档_使用Sphinx构建自定义文档工作流
  7. Spring Boot 2.0 多数据源编程 jdbcUrl is required with driverClassName
  8. Go Elasticsearch index CRUD
  9. 【PostgreSQL-9.6.3】psql常用命令
  10. 洛谷P2347 砝码称重 [2017年4月计划 动态规划01]
  11. mount failed: mount failed: exit status 1
  12. 介绍一种计算机病毒并如何清理,如何清除顽固的计算机病毒和木马
  13. 打印机出现另存为xps_win10系统打印文件弹出另存为xps/pdf的处理方法
  14. 支付宝第三方登录接口 php,PHP调用支付宝支付接口操作步骤
  15. 访问yy直播页面点击播放无响应分析
  16. delphi低级键盘钩子(delphi2009测试通过)
  17. matlab 有限元分析
  18. iNFTnews|日本即时通讯软件LINE推出NFT市场
  19. 福昕阅读器打不开html文件吗,福昕阅读器打不开XP台式
  20. 江南大学计算机阶段测试题,江南大学2016.09计算机应用基础(专科类)第2阶段测试题...

热门文章

  1. C练题笔记之:Leetcode-137. 只出现一次的数字 II
  2. ITK 2D图像刚性配准
  3. C#中设计器的控件事件转到逻辑代码
  4. COI 2020 Semafor(矩阵乘法+优化)
  5. Fliqlo——翻页时钟屏保(最新版本,附有链接)
  6. Java 经典小题篇
  7. HTML5 Canvas 绘制库存变化折线 计算出库存周转率
  8. php查询mysql充值_PHP + MYSQL 实现 用户注册/登录/充值 功能
  9. 海康设备对接sdk错误码汇总 v6.0
  10. Project ERROR: Cannot run compiler 'cl'. Maybe you forgot to setup the environment?