介绍
前一段时间公司要求登录和鉴权使用SpringSecurity,看了很多都感觉不太适合企业开发,于是自己整理了一下,基于@PreAuthorize注解鉴权,大大的方便的权限控制。
设计技术
本项目使用mysql数据库、mybatis-plus、redis缓存、jwt、hutool工具包、fastjson2。
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 数据库相关-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- lombook-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.15</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.13</version>
</dependency>
<!--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>
<!--JSON解析工具-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.10</version>
</dependency>
数据库
需要四张表,如下图:
sys_user(用户表)
sys_role(角色表)
sys_permission(权限表)
sys_user_role(用户角色表)
搭建环境
自己搭建哦!!
配置类
import com.qian.chatgpt.entity.info.SecurityInfo;
import com.qian.chatgpt.handler.AuthenticationHandler;
import com.qian.chatgpt.handler.CustomLogoutHandler;
import com.qian.chatgpt.handler.CustomLogoutSuccessHandler;
import lombok.RequiredArgsConstructor;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author Qiansheng
* @date 2023/4/23
* @description security配置类
*/
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解全局安全认证
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final AuthenticationHandler authenticationHandler;
private final com.qian.chatgpt.filter.AuthenticationTokenFilter AuthenticationTokenFilter;
private final SecurityInfo securityInfo;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
String[] anonymouPaths = securityInfo.getPermitAllPaths().toArray(new String[0]);
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类,指定异常处理实现类
.exceptionHandling().authenticationEntryPoint(authenticationHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
//设置匿名访问
.antMatchers(anonymouPaths).permitAll()
//其他全部拦截认证
.anyRequest()
.authenticated()
.and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
//将认证过滤器添加到security中
http.addFilterBefore(AuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 设置加密方式
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
是不是有很多爆红呢?问题比大,我们一个一个来
UserDetailsService:此类是用来从数据库查询用户信息的
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.qian.chatgpt.entity.user.SysPermission;
import com.qian.chatgpt.entity.user.SysUser;
import com.qian.chatgpt.entity.user.SysUserRole;
import com.qian.chatgpt.mapper.SysUserMapper;
import com.qian.chatgpt.service.user.ISysPermissionService;
import com.qian.chatgpt.service.user.ISysUserRoleService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author : Qiansheng
* @创建时间 : 2023/4/23
* @描述信息 : security 对比用户名类
*/
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserDetailsServiceImpl implements UserDetailsService {
private final SysUserMapper sysUserMapper;
private final ISysPermissionService ISysPermissionService;
private final ISysUserRoleService sysUserRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.selectOne(Wrappers.lambdaQuery(SysUser.class).eq(SysUser::getUsername,username));
//判断用户名是否存在 如存在就将信息存储
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
String permissionList="";
//查询权限
SysUserRole userRole = sysUserRoleService.getOne(Wrappers.lambdaQuery(SysUserRole.class).eq(SysUserRole::getUserId, user.getId()));
List<SysPermission> permissions = ISysPermissionService.list(Wrappers.lambdaQuery(SysPermission.class).eq(SysPermission::getRoleId, userRole.getRoleId()));
for (SysPermission permission : permissions) {
permissionList=permissionList+permission.getPermissionKey()+",";
}
if(permissionList.length()!=0){
permissionList = permissionList.substring(0,permissionList.length()-1);
}
User user1 = new User(user.getUsername(), user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(permissionList));
return user1;
}
}
AuthenticationHandler:此类是用来处理认证失败的异常处理类
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import com.qian.chatgpt.util.R;
import com.qian.chatgpt.util.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
/**
* @author : Qiansheng
* @创建时间:2023/4/23
* @描述信息: security 拦截后返回内容
*/
@Component
@Slf4j
public class AuthenticationHandler implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
ServletUtil.renderString(HttpStatus.HTTP_FORBIDDEN, response, JSONUtil.toJsonStr(R.fail(HttpStatus.HTTP_FORBIDDEN, "非法请求:" +e.getMessage())));
}
}
SecurityInfo: 放行路径信息实体
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* @author gqs
* @date 2023/5/16 16:49
* @description 放行路径信息实体
*/
@Component
@ConfigurationProperties("security")
@Data
public class SecurityInfo {
/**
* 放行路径
*/
private Set<String> permitAllPaths;
}
所有需要放行的路径放在yml配置文件中:
AuthenticationTokenFilter:用户请求认证拦截器
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.qian.chatgpt.util.JwtUtil;
import com.qian.chatgpt.util.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author gqs
* @date 2023/5/16 16:49
* @description 用户请求token认证拦截器
*/
@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//使用token从redis获取jwt
String cacheObject = redisCache.getCacheObject(token);
//使用jwt获取用户信息并处理
Claims claims = null;
try {
claims = JwtUtil.parseJWT(cacheObject);
} catch (Exception e) {
e.printStackTrace();
}
if(claims==null){
filterChain.doFilter(request, response);
return;
}
String usersubject = claims.getSubject();
JSONObject entries = JSON.parseObject(usersubject);
JSONArray authorities = entries.getJSONArray("authorities");
String authlist="";
for (int i = 0; i < authorities.size(); i++) {
JSONObject o = (JSONObject) authorities.get(i);
authlist=authlist+o.getString("authority")+",";
}
if(authlist.length()!=0){
authlist = authlist.substring(0,authlist.length()-1);
}
User user = new User(entries.getString("username"), "", AuthorityUtils.commaSeparatedStringToAuthorityList(authlist));
//存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
好了,配置信息差不多就这些。
登录
登录的service:
public R login(SysUser tUser) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(tUser.getUsername(), tUser.getPassword());
authenticate = authenticationManagerBean.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//获取用户信息
User user = (User) authenticate.getPrincipal();
//将用户信息存入jwt
log.info(user.toString());
// hotool工具解析json少东西 更换为fastjson
String s1 = JSON.toJSONString(user);
String jwt = JwtUtil.createJWT(s1,3600000L);
//将jwt存入redis
String token = UUID.randomUUID().toString().replaceAll("-","");
redisCache.setCacheObject(token, jwt,3600000L);
//把token响应给前端
HashMap<String, String> map = new HashMap();
map.put("token", token);
return R.data(map);
}
controller
import com.qian.chatgpt.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author gqs
* @作者: Qiansheng
* @创建时间:2023/4/23
* @描述信息: TODO
*/
@Slf4j
@RestController
@RequestMapping("/admin")
@CrossOrigin //关闭跨域验证
@Api(tags = "管理员管理")
public class AdminTestController {
@PreAuthorize("hasAnyAuthority('admin:sel')")
@ApiOperation(value = "管理员权限测试", notes = "管理员权限测试")
@GetMapping("/test")
public R test() {
return R.success("admin权限测试成功");
}
}
注意:@PreAuthorize(“hasAnyAuthority(‘admin:sel’)”) 中的admin:sel是权限