目录
目录
前言:
一.所需依赖
二.application.properties
三.工具类
3.1ApplicationContextUtils
3.2JwtUtils
3.3ResponseResult
3.4ResponseStatus
3.5RsaUtils
四.UserDetailServiceImpl
五.成功处理器
六.SecurityConfig
七. filter
八.项目结构
前后端分离项目运行流程
我之前的博客写了一篇前后端不分离的权限验证,前后端分离就是在原来的基础上加上发送token给前端,前端发送请求给服务器需要携带token。不太懂的朋友请先移步(29条消息) springsecurity基于数据库中的用户信息实现登陆_代码大帝的博客-CSDN博客
嫌麻烦的同学也可以直接把配置代码拿来用,代码都有写的。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
com.dmdd
javatestspringsecurity
0.0.1-SNAPSHOT
javatestspringsecurity
javatestspringsecurity
1.8
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
org.projectlombok
lombok
true
com.baomidou
mybatis-plus-boot-starter
3.5.2
mysql
mysql-connector-java
runtime
io.jsonwebtoken
jjwt
0.9.0
joda-time
joda-time
2.9.9
com.baomidou
mybatis-plus-annotation
3.5.2
compile
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-maven-plugin
直接拿来用。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/edu_user?serverTimezone=UTC&useUnicode=true&useSSL=false&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=jly720609
mybatis-plus.type-aliases-package=com.dmdd.javatestspringsecurity.entity
mybatis-plus.mapper-locations=classpath:mapper/*.xml
懂的都懂
包含生成公钥秘钥,对象转json等一系列方法
package com.dmdd.javatestspringsecurity.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 应用程序上下文工具
* 程序启动后,会创建ApplicationContext对象
* ApplicationContextAware能感知到ApplicationContext对象
* 自动调用setApplicationContext方法
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
//系统的IOC容器
private static ApplicationContext applicationContext = null;
//感知到上下文后,自动调用,获得上下文
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtils.applicationContext = applicationContext;
}
//返回对象
public static T getBean(Class tClass){
return applicationContext.getBean(tClass);
}
}
package com.dmdd.javatestspringsecurity.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* JWT工具类
*/
public class JwtUtils {
public static final String JWT_KEY_USERNAME = "username";
public static final int EXPIRE_MINUTES = 120;
/**
* 私钥加密token
*/
public static String generateToken(String username, PrivateKey privateKey, int expireMinutes) {
return Jwts.builder()
.claim(JWT_KEY_USERNAME, username)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 从token解析用户
*
* @param token
* @param publicKey
* @return
* @throws Exception
*/
public static String getUsernameFromToken(String token, PublicKey publicKey){
Jws claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims body = claimsJws.getBody();
String username = (String) body.get(JWT_KEY_USERNAME);
return username;
}
}
package com.dmdd.smart_community.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 响应数据封装对象
*/
@Data
public class ResponseResult {
/**
* 状态信息
*/
private ResponseStatus status;
/**
* 数据
*/
private T data;
public ResponseResult(ResponseStatus status, T data) {
this.status = status;
this.data = data;
}
/**
* 返回成功对象
* @param data
* @return
*/
public static ResponseResult ok(T data){
return new ResponseResult<>(ResponseStatus.OK, data);
}
/**
* 返回错误对象
* @param status
* @return
*/
public static ResponseResult error(ResponseStatus status){
return new ResponseResult<>(status,status.getMessage());
}
/**
* 返回错误对象
* @param status
* @return
*/
public static ResponseResult error(ResponseStatus status,String msg){
return new ResponseResult<>(status,msg);
}
/**
* 向流中输出结果
* @param resp
* @param result
* @throws IOException
*/
public static void write(HttpServletResponse resp, ResponseResult result) throws IOException {
//设置返回数据的格式
resp.setContentType("application/json;charset=UTF-8");
//jackson是JSON解析包,ObjectMapper用于解析 writeValueAsString 将Java对象转换为JSON字符串
String msg = new ObjectMapper().writeValueAsString(result);
//用流发送给前端
resp.getWriter().print(msg);
resp.getWriter().close();
}
}
package com.dmdd.javatestspringsecurity.util;
/**
* 响应状态枚举
*/
public enum ResponseStatus {
/**
* 内置状态
*/
OK(200,"操作成功"),
INTERNAL_ERROR(500000,"系统错误"),
BUSINESS_ERROR(500001,"业务错误"),
LOGIN_ERROR(500002,"账号或密码错误"),
NO_DATA_ERROR(500003,"没有找到数据"),
PARAM_ERROR(500004,"参数格式错误"),
AUTH_ERROR(401,"没有权限,需要登录");
//响应代码
private Integer code;
//响应消息
private String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
ResponseStatus(Integer status, String message) {
this.code = status;
this.message = message;
}
}
package com.dmdd.javatestspringsecurity.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA工具类
*/
public class RsaUtils {
public static final String RSA_SECRET = "blbweb@#$%"; //秘钥
public static final String RSA_PATH = System.getProperty("user.dir")+"/rsa/";//秘钥保存位置
public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pubKey.rsa";//公钥路径
public static final String RSA_PRI_KEY_PATH = RSA_PATH + "priKey.rsa";//私钥路径
public static PublicKey publicKey; //公钥
public static PrivateKey privateKey; //私钥
/**
* 类加载后,生成公钥和私钥文件
*/
static {
try {
File rsa = new File(RSA_PATH);
if (!rsa.exists()) {
rsa.mkdirs();
}
File pubKey = new File(RSA_PUB_KEY_PATH);
File priKey = new File(RSA_PRI_KEY_PATH);
//判断公钥和私钥如果不存在就创建
if (!priKey.exists() || !pubKey.exists()) {
//创建公钥和私钥文件
RsaUtils.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
}
//读取公钥和私钥内容
publicKey = RsaUtils.getPublicKey(RSA_PUB_KEY_PATH);
privateKey = RsaUtils.getPrivateKey(RSA_PRI_KEY_PATH);
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(1024, secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
package com.dmdd.javatestspringsecurity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dmdd.javatestspringsecurity.entity.User;
import com.dmdd.javatestspringsecurity.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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;
/**
* 实现自定义用户登录逻辑
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//按用户名查询用户信息
User user = userService.getOne(new QueryWrapper().lambda().eq(User::getUsername, s));
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
//查询所有用户权限 List --> xx,xxx,xxx,xx --> List
List authList = userService.getAuthoritiesByUsername(s);
String auths = String.join(",", authList);
//把用户信息包装到UserDetails的实现类User中
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(auths));
}
}
1.这个类需要实现UserDetailService接口
2.重写loadUserBYUsername方法
3.将用户名,密码,授权信息包装到UserDetails的实现类User中。
package com.dmdd.javatestspringsecurity.config;
import com.dmdd.javatestspringsecurity.entity.vo.UserTokenVO;
import com.dmdd.javatestspringsecurity.util.JwtUtils;
import com.dmdd.javatestspringsecurity.util.ResponseResult;
import com.dmdd.javatestspringsecurity.util.RsaUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录成功处理器
*/
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
//登录成功的回调
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//获得用户名
User user = (User) authentication.getPrincipal();
//生成token字符串
String token = JwtUtils.generateToken(user.getUsername(), RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES);
log.info("生成token:{}",token);
//发送token给前端
ResponseResult.write(httpServletResponse,ResponseResult.ok(new UserTokenVO(user.getUsername(),token)));
}
}
因为项目前后端分离,服务器内不再拥有前端代码。所以不能直接用url的形式进行登陆成功后的跳转。因此采用成功处理器将用户名,秘钥,以及存活时间包装成token响应给前端
package com.dmdd.javatestspringsecurity.config;
import com.dmdd.javatestspringsecurity.filter.RequestAuthenticationFilter;
import com.dmdd.javatestspringsecurity.util.ResponseResult;
import com.dmdd.javatestspringsecurity.util.ResponseStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* SpringSecurity的核心配置
*/
//启动权限控制的注解
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
//启动Security的验证
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//提供密码编码器
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
//配置验证用户的账号和密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据库用户验证
auth.userDetailsService(userDetailsService);
}
//配置访问控制
@Override
protected void configure(HttpSecurity http) throws Exception {
//给请求授权
http.authorizeRequests()
//给登录相关的请求放行
.antMatchers("/login","/logout").permitAll()
//访问控制
//其余的都拦截
.anyRequest().authenticated()
.and()
//配置自定义登录
.formLogin()
.successHandler(loginSuccessHandler)//成功处理器
.failureHandler(((httpServletRequest, httpServletResponse, e) -> { //登录失败处理器
ResponseResult.write(httpServletResponse,ResponseResult.error(ResponseStatus.LOGIN_ERROR));
}))
.and()
.logout() //配置注销
.logoutSuccessHandler(((httpServletRequest, httpServletResponse, authentication) -> { //注销成功
ResponseResult.write(httpServletResponse,ResponseResult.ok(ResponseStatus.OK));
}))
.clearAuthentication(true) //清除验证信息
.and()
.cors() //配置跨域
.configurationSource(corsConfigurationSource())
.and()
.csrf().disable() //停止csrf
.sessionManagement() //session管理
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //无状态,不使用session
.and()
.addFilter(new RequestAuthenticationFilter(authenticationManager())) //添加自定义验证过滤器
;
}
/**
* 跨域配置对象
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
//配置允许访问的服务器域名
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
可以直接拿来用。
package com.dmdd.javatestspringsecurity.filter;
import com.dmdd.javatestspringsecurity.service.IUserService;
import com.dmdd.javatestspringsecurity.util.ApplicationContextUtils;
import com.dmdd.javatestspringsecurity.util.JwtUtils;
import com.dmdd.javatestspringsecurity.util.RsaUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
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.List;
/**
* 请求验证过滤器
*/
@Slf4j
public class RequestAuthenticationFilter extends BasicAuthenticationFilter {
public static final String AUTH_HEADER = "Authorization";
//通过工具类获得service对象
private IUserService userService = ApplicationContextUtils.getBean(IUserService.class);
public RequestAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
//请求的过滤
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//从请求头获得token
String token = request.getHeader(AUTH_HEADER);
if(StringUtils.isEmpty(token)){
//从请求参数获得token
token = request.getParameter(AUTH_HEADER);
}
//如果读取不到,就拦截
if(StringUtils.isEmpty(token)){
log.info("读取不到token,请求{}被拦截",request.getRequestURL());
chain.doFilter(request,response);
return;
}
try {
//对token进行解析
String username = JwtUtils.getUsernameFromToken(token, RsaUtils.publicKey);
//将用户的权限查询出来
List authList = userService.getAuthoritiesByUsername(username);
List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", authList));
//创建通行证
UsernamePasswordAuthenticationToken authToken = new
UsernamePasswordAuthenticationToken(username,"",authorities);
//把通行证交给Security
SecurityContextHolder.getContext().setAuthentication(authToken);
}catch (Exception ex){
log.error("解析token失败",ex);
}
chain.doFilter(request,response);
}
}
主要作用就是解析前端发过来的token,验证是否通过登陆,防止异常数据攻击服务器
注意:查询用户权限的方法需要自己编写
通过用户名查询该用户有哪些权限。然后交给服务器设置哪些接口需要哪些权限可以使用