spring boot中常用的安全框架
Security 和 Shiro 框架
Security 两大核心功能 认证 和 授权
重量级
Shiro 轻量级框架 不限于web 开发
在不使用安全框架的时候
一般我们利用过滤器和 aop自己实现 权限验证 用户登录
Security 实现逻辑
- 输入用户名和密码 提交
- 把提交用户名和密码封装对象
3、4 调用方法实现验证
5、调用方法、根据用户米查询用户信息
6、查询用户信息返回对象
7、密码比较
8、填充回、返回
9、返回对象放到上下文对象里面
引入依赖
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
刚开始测试的话 默认密码在控制台
把Security框架 使用到自己项目中
具体核心组件
- 第一步、登录接口 判断用户名和密码
自定义以下组件
1、 创建自己相对应的User 实体类 继承 org.springframework.security.core.userdetails.User
在里面定义自己的实体类字段和实现一个CustomUser()方法
package com.oa.security.custom;
import com.oa.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class CustomUser extends User {
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
*/
private SysUser sysUser;
public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
}
2、 重写 loadUserByUsername 方法
自定义一个 UserDetailsService 接口
继承org.springframework.security.core.userdetails.UserDetailsService 下的这个类
重写 UserDetailsService里的 loadUserByUsername方法
自定义一个 UserDetailsService 接口 的具体实现类 就是去数据库验证的实现类 比如 UserDetailsServiceImpl
在这个类 实现 loadUserByUsername 方法
实现后 最后返回 第一步创建的自定义的User 实体类
因为自己自定义的User类继承了UserDetails类
所以等于把数据交给了Security框架
UserDetailsService 接口
package com.oa.security.custom;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService {
/**
* 根据用户名获取用户对象(获取不到直接抛异常)
*/
@Override
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsServiceImpl实现接口
package com.erp.base.service;
import com.erp.api.entities.base.base.YsUser;
import com.erp.api.inteface.base.base.IYsUserService;
import com.erp.init.security.enities.CustomUser;
import com.erp.init.security.service.UserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.ArrayList;
import java.util.List;
/**
* User: Json
* <p>
* Date: 2024/3/3
* security 安全框架
**/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IYsUserService iYsUserService;
@Override
public UserDetails loadUserByUsername(String username) throws AuthenticationException {
//根据用户名进行查询
YsUser ysUser = iYsUserService.getUserByUserName(username);
if(ObjectUtils.isEmpty(ysUser)) {
//在用户登录的方法里 如果想让security 安全框架 正确抛出异常给前端 需要使用 BadCredentialsException
// 只有使用了BadCredentialsException异常类
// unsuccessfulAuthentication 这个方法里才能接收到异常 统一抛出
// unsuccessfulAuthentication 这个方法是 认证失败调用的统一方法
throw new BadCredentialsException("用户名不存在!");
}
// if(ysUser.getStatus().intValue() == 0) {
// throw new ErpRuntimeException("账号已停用");
// }
//根据userid查询用户操作权限数据
List<String> userPermsList =new ArrayList<>();
// List<String> userPermsList = sysMenuService.findUserPermsByUserId(ysUser.getId());
//创建list集合,封装最终权限数据
List<SimpleGrantedAuthority> authList = new ArrayList<>();
if(!CollectionUtils.isEmpty(userPermsList)){
//查询list集合遍历
for (String perm : userPermsList) {
authList.add(new SimpleGrantedAuthority(perm.trim()));
}
}
return new CustomUser(ysUser, authList);
}
}
3、 自定义一个秘密校验器 CustomMd5PasswordEncoder 实现 org.springframework.security.crypto.password.PasswordEncoder 接口
package com.oa.security.custom;
import com.oa.common.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
4、 创建一个过滤器 来验证token 比如 TokenLoginFilter
继承 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
在方法里 定义四个方法 都是重写父类方法
定义一个构造方法
登录认证方法 获取输入的用户名和密码,调用方法认证 attemptAuthentication() 进行账号密码认证 认证实际就是执行了我们设置的第一步的内容
认证成功调用方法 successfulAuthentication() 如果认证成功 这个方法里 就处理比如生成token 存入权限 等
认证失败调用方法 unsuccessfulAuthentication() 如果认证失败 这个方法里 就处理失败的逻辑
package com.erp.init.security.filter;
import com.alibaba.fastjson.JSON;
import com.erp.api.out.R;
import com.erp.api.out.ResultCode;
import com.erp.api.request.LoginRequest;
import com.erp.api.response.ResponseUtil;
import com.erp.init.security.enities.CustomUser;
import com.erp.init.security.jwt.JwtHelper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* User: Json
* <p>
* Date: 2024/3/3
* 用于登录认证
**/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private RedisTemplate redisTemplate;
//构造方法
public TokenLoginFilter(AuthenticationManager authenticationManager,
RedisTemplate redisTemplate) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口及提交方式,可以指定任意路径 安全框架会从这个接口里取相应数据
//这里 setRequiresAuthenticationRequestMatcher 定义了 /login
// config文件中 .antMatchers("/admin/**").permitAll() 这里就不需要配置了
// 同样 如果配置文件中 定义了 api前缀 这里是需要省略不写的
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
this.redisTemplate = redisTemplate;
}
//登录认证
//获取输入的用户名和密码,调用方法认证
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
try {
//获取用户信息 实际就是获取的登录接口的 那个实体类里的数据
LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
//封装对象 然后把用户名和密码 传入进去
Authentication authenticationToken =
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
//调用方法
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//认证成功调用方法
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication auth)
throws IOException, ServletException {
//获取当前用户
CustomUser customUser = (CustomUser)auth.getPrincipal();
//生成token
String token = JwtHelper.createToken(customUser.getSysUser());
//获取当前用户权限按钮数据,放到Redis里面 key:username value:权限数据
redisTemplate.opsForValue().set(customUser.getUsername(),
JSON.toJSONString(customUser.getAuthorities()));
//返回
Map<String,Object> map = new HashMap<>();
map.put("token",token);
ResponseUtil.out(response, R.data(map));
}
//认证失败调用方法
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed)
throws IOException, ServletException {
//System.out.println(failed.getMessage());
ResponseUtil.out(response,R.fail(ResultCode.FAILURE,failed.getMessage()));
}
}
以上就是用户调用登录接口 利用Security框架 完成登录
第二步、认证解析token组件:解决调用其他接口时 看看用户有没有登录
判断请求头是否有token 如果有,认证完成 (通俗一点就是 判断当前是否登录)
自定义一个 TokenAuthenticationFilter 继承 org.springframework.web.filter.OncePerRequestFilter;
重写 里面的方法 doFilterInternal()
package com.erp.init.security.filter;
import com.alibaba.fastjson.JSON;
import com.erp.api.out.R;
import com.erp.api.out.ResultCode;
import com.erp.api.response.ResponseUtil;
import com.erp.init.security.LoginHelper.LoginUserInfoHelper;
import com.erp.init.security.jwt.JwtHelper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* User: Json
* <p>
* Date: 2024/3/3
* 用于处理基于令牌的身份验证请求
**/
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
//如果是登录接口,直接放行
if("/api/login".equals(request.getRequestURI()) || request.getRequestURI().startsWith("/api/admin/")) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, R.fail(ResultCode.NO_USER));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//请求头是否有token
String token = request.getHeader("token");
if(!StringUtils.isEmpty(token)) {
String username = JwtHelper.getUsername(token);
if(!StringUtils.isEmpty(username)) {
//当前用户信息放到ThreadLocal里面 不放在 ThreadLocal 直接从请求头里取也是可以的
// ThreadLocal 设置的变量对于每个线程都是独立的,线程之间的变量不会相互干扰
LoginUserInfoHelper.setUserId(JwtHelper.getUserId(token));
LoginUserInfoHelper.setUsername(username);
LoginUserInfoHelper.setTenantId(JwtHelper.getTenantId(token));
//通过username从redis获取权限数据
String authString = (String)redisTemplate.opsForValue().get(username);
//把redis获取字符串权限数据转换要求集合类型 List<SimpleGrantedAuthority>
if(!StringUtils.isEmpty(authString)) {
List<Map> maplist = JSON.parseArray(authString, Map.class);
// System.out.println(maplist);
List<SimpleGrantedAuthority> authList = new ArrayList<>();
for (Map map:maplist) {
String authority = (String)map.get("authority");
authList.add(new SimpleGrantedAuthority(authority));
}
return new UsernamePasswordAuthenticationToken(username,null, authList);
} else {
return new UsernamePasswordAuthenticationToken(username,null, new ArrayList<>());
}
}
}
return null;
}
}
第三步、在配置类配置相关认证类
package com.erp.init.security.config;
import com.erp.init.security.filter.TokenAuthenticationFilter;
import com.erp.init.security.filter.TokenLoginFilter;
import com.erp.init.security.service.UserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
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.builders.WebSecurity;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* User: Json
* <p>
* Date: 2024/3/3
**/
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true) //它允许在方法级别进行安全性控制,用于按钮权限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RedisTemplate redisTemplate;
//导入这个接口的时候 有可能会报错找不到这个类
//我们有自己创建了一个 UserDetailsService 接口
// 如果导入我们的 也会报错
// 解决方案就是 把我们自定义的 UserDetailsService 接口 继承 框架下的UserDetailsService
// 就可以了
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//http 不需要通过认证即可访问,比如登录页面通常是不需要认证的
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf跨站请求伪造
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
// 如果你在配置文件中
//server:
// port: 18181
// servlet:
// context-path: /api
// 配置了 api 前缀 context-path: /api
// 在使用.antMatchers 这个方法的时候 不需要加 /api 直接写控制器里的路径就好
.antMatchers("/admin/**").permitAll()
// .permitAll() 这种权限有很多中 方法 也有根据角色的方法 使用到 百度即可
// 注意: 如果使用了自定义的过滤器 比如 TokenAuthenticationFilter 这种 因为过滤器的优先级高
//如果需要哪些接口 不需要token验证 直接放行 那就需要在自定义的过滤器 进行接口判断 放行
// 过滤器 判断后 会再走这里的配置 .antMatchers("/admin/**").permitAll() 因为自定义过滤器优先级高
// 所以 如果自定义了 过滤器 security框架放行接口 需要配置两个地方
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(redisTemplate),
UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager(),redisTemplate));
//禁用session
// 禁用 Session 主要是为了实现基于 Token 的认证机制,提高应用程序的安全性和性能。
//
// 当禁用 Session 后,意味着每个请求都将被视为无状态的,即不再依赖于服务器端的会话状态。
// 相反,每个请求都需要携带有效的认证 Token 来进行身份验证。这种方式被称为“无状态身份验证”,
// 它将认证信息完全交给客户端处理,服务器不再存储用户的认证状态,从而减轻了服务器的负担,并提高了系统的可伸缩性和性能。
//
// 禁用 Session 适用于前后端分离的架构和无状态的 RESTful API,
// 其中客户端通常会在每个请求中携带 Token 来进行身份验证。
// 通过禁用 Session,可以更好地支持这种架构,并使应用程序更加安全和高效。
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger相关请求
* web 主要是为了过滤通常用于配置一些静态资源,如图片、样式表、脚本文件等
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/swagger-resources/**", "/swagger-ui.html/**", "/doc.html");
}
}
用户授权
代码在上面的类里已经存在,这里只截图提现
比如按钮权限 哪些按钮课余访问
这些按钮权限 在 数据库存着 一般是给前端使用
但是这种操作 如果懂技术的人 可以绕过前端 直接访问后端api接口
为了解决这个问题 所以后端也需要做用户授权
第一步 在查询用户名密码验证的时候 把用户的按钮权限查询出来
一个按钮一般对应的是一个接口
然后交给 Security框架
第二步验证成功后,给前端返回token 那里从Security框架里拿出 按钮权限
然后存到redis里
第三步用户在请求接口的时候 这个时候把从redis里把当前用户的按钮权限拿出来
然后验证
第四步 在Security配置文件中添加上redis配置
第五步 在控制器里 controller 里添加权限注解
@PreAuthorize(“hasAuthority(‘bnt.sysRole.list’)”)
//bnt.sysRole.list 这个值 就是存在数据库里的按钮的字段 前端和后端可以共同使用
最后在定义以下异常处理类
/**
* spring security异常
* @param e
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public Result error(AccessDeniedException e) throws AccessDeniedException {
return Result.fail().code(205).message("没有操作权限");
}
这样 验证和 授权 就全部完成了
用到的工具类:
JwtHelper
package com.erp.init.security.jwt;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
/**
* User: Json
* <p>
* Date: 2024/3/3
**/
public class JwtHelper {
private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
private static String tokenSignKey = "123456";
//根据用户id和用户名称生成token字符串
public static String createToken(Integer userId, String username) {
String token = Jwts.builder()
//分类
.setSubject("AUTH-USER")
//设置token有效时长
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
//设置主体部分
.claim("userId", userId)
.claim("username", username)
//签名部分
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从生成token字符串获取用户id
public static Long getUserId(String token) {
try {
if (StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer) claims.get("userId");
return userId.longValue();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//从生成token字符串获取用户名称
public static String getUsername(String token) {
try {
if (StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
String token = JwtHelper.createToken(6, "li4");
System.out.println(token);
Long userId = JwtHelper.getUserId(token);
String username = JwtHelper.getUsername(token);
System.out.println(userId);
System.out.println(username);
}
}
ResponseUtil
package com.erp.api.response;
import com.erp.api.out.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* User: Json
* <p>
* Date: 2024/3/3
**/
public class ResponseUtil {
// R 是返回的数据格式
public static void out(HttpServletResponse response, R r) {
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), r);
} catch (IOException e) {
e.printStackTrace();
}
}
}
LoginUserInfoHelper 类
package com.erp.init.security.LoginHelper;
/**
* User: Json
* <p>
* Date: 2024/3/3
**/
public class LoginUserInfoHelper {
private static ThreadLocal<Integer> userId = new ThreadLocal<Integer>();
private static ThreadLocal<String> username = new ThreadLocal<String>();
public static void setUserId(Integer _userId) {
userId.set(_userId);
}
public static Integer getUserId() {
return userId.get();
}
public static void removeUserId() {
userId.remove();
}
public static void setUsername(String _username) {
username.set(_username);
}
public static String getUsername() {
return username.get();
}
public static void removeUsername() {
username.remove();
}
}