Spring Security 前后端分离下的权限控制
程序员文章站
2022-07-10 10:41:46
...
Spring Security 前后端分离下的权限控制
简单来说,前后端分离的项目和前后端不分离的项目区别在于:
前者是通过响应状态来驱动业务的,而后者可以直接进行页面的跳转,进而驱动业务的。
依赖
<!--SpringBoot Security模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- kaptcha验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
这里使用redis,实现用户令牌token的免密登录。
SecurityConfiguration:springsecurity的配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
MyAuthenctiationEntryPointHandler myAuthenctiationEntryPointHandler;//未登录
@Autowired
MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;//登陆成功
@Autowired
MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;//登录失败
@Autowired
MyAuthenctiationDeniedHandler myAuthenctiationDeniedHandler;//无权访问
@Autowired
MyAuthenctiationLogoutSuccessHandler myAuthenctiationLogoutSuccessHandler;//退出成功
@Autowired
MyAuthenctiationInvalidSessionStrategy mMyAuthenctiationInvalidSessionStrategy;//session到期
@Autowired
MyAuthenctiationSessionInformationExpiredStrategy myAuthenctiationSessionStrategy;//session到期,被登陆
//注入封装账号信息的处理bean
@Bean
UserDetailsService detailsService() {
return new AuthUserDetailsService();
}
/*配置用户登录拦截器,解决username无法获取的问题*/
@Bean
LoginAuthenticationFilter loginAuthenticationFilter() throws Exception {
LoginAuthenticationFilter filter = new LoginAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationSuccessHandler(myAuthenctiationSuccessHandler);//登录成功
filter.setAuthenticationFailureHandler(myAuthenctiationFailureHandler);//登录失败
return filter;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/attendance/userinfo/loginValidateCode","/swagger-ui.html","/swagger-resources/configuration/security","/swagger-resources/configuration/ui","/swagger*//**","/api/v1/login","/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security", "/swagger-ui.html", "/webjars/**");
}
//用户授权操作
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Spring Security 被启用");
http
.addFilterAt(loginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/attendance/userinfo/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenctiationEntryPointHandler)//未登录402
.accessDeniedHandler(myAuthenctiationDeniedHandler)//无权访问403
.and()
.formLogin().loginPage("/attendance/userinfo/loginUser")
.successHandler(myAuthenctiationSuccessHandler)//登陆成功200
.failureHandler(myAuthenctiationFailureHandler)//登陆失败401
.permitAll()
.and()
.sessionManagement()//session到期提示
.invalidSessionStrategy(mMyAuthenctiationInvalidSessionStrategy)//session到期101
.and()
.requestCache().disable()
.logout()
.logoutSuccessHandler(myAuthenctiationLogoutSuccessHandler)//退出登陆200
.and()
.csrf().disable(); //关闭CSRF
}
//设置加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityContextRepository securityContextRepository() {
//设置对spring security的UserDetails进行session保存,这个必须要有,不然不会保存至session对应的缓存redis中
HttpSessionSecurityContextRepository httpSessionSecurityContextRepository =
new HttpSessionSecurityContextRepository();
return httpSessionSecurityContextRepository;
}
//加入中间验证层,可实现自定义验证用户等信息
@Bean
public AuthenticationProvider authenticationProvider() {
AuthenticationProvider provider =new MyAuthenticationProvider();
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}
LoginAuthenticationFilter:解决前段端分离项目中参数无法正常注入的问题
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
RedisTemplate redisTemplate;
@Autowired
UserService userService;
public static LoginAuthenticationFilter loginAuthenticationFilter;
// 关键3
@PostConstruct
public void init() {
loginAuthenticationFilter = this;
loginAuthenticationFilter.userService = this.userService;
}
public LoginAuthenticationFilter() {
AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher("/attendance/userinfo/loginUser", "POST");
this.setRequiresAuthenticationRequestMatcher(requestMatcher);
this.setAuthenticationManager(getAuthenticationManager());
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {//必须post登录
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String token =null;
String username = null;
String password = null;
String vercode = null;
//以json形式处理数据
try {
//将请求中的数据转为map
Map<String,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
username = map.get("userName");
password = map.get("password");
vercode = map.get("vercode");
token= map.get("token");
request.getSession().setAttribute("vercode", vercode);
} catch (IOException e) {
e.printStackTrace();
}
if(token ==null){//使用密码进行登录
//如果是application/json类型,做如下处理
if(request.getContentType() != null && (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE))){
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}else {//使用用户令牌进行登录
String usernametemp = null;
if(redisTemplate.opsForValue().get("token_"+token)!=null){
usernametemp =redisTemplate.opsForValue().get("token_"+token).toString();
}
if(StringUtils.isBlank(usernametemp)){
throw new UsernameNotFoundException("用户令牌过期");
}
request.getSession().setAttribute("token",token);
UserInfoPojo userInfoPojo =userService.loginbyUsername(usernametemp);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(usernametemp, userInfoPojo.getPassword());
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
//否则使用官方默认处理方式
return super.attemptAuthentication(request, response);
}
}
MyAuthenticationProvider:自定义的用户登录部分
/**
* 自定义校验-密码、图片验证码
*
*/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
//日志记录器
private static Logger logger= LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Autowired
private AuthUserDetailsService authUserDetailsService;
@Autowired
HttpServletRequest httpServletRequest;
@Autowired
PasswordEncoder passwordEncoder;
/**
* 登录验证码SessionKey
*/
public static final String LOGIN_VALIDATE_CODE = "login_validate_code";
/**
* 自定义验证方式
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String token =null;
String password =null;
UserDetails user =null;
if(httpServletRequest.getSession().getAttribute("token")!=null){
token = httpServletRequest.getSession().getAttribute("token").toString();
}
if(StringUtils.isBlank(token)){//使用密码进行登录 否则就是使用用户令牌进行登录
String requestCode = httpServletRequest.getSession().getAttribute("vercode").toString();
HttpSession session = httpServletRequest.getSession();
String saveCode = httpServletRequest.getSession().getAttribute(LOGIN_VALIDATE_CODE).toString();
//获取到session验证码后随时清除
if(!StringUtils.isEmpty(saveCode)) {
session.removeAttribute(LOGIN_VALIDATE_CODE);//captcha
session.removeAttribute("vercode");
}
logger.info("requestCode:"+requestCode+",saveCode:"+saveCode);
if(StringUtils.isEmpty(saveCode) || StringUtils.isEmpty(requestCode) || !requestCode.equals(saveCode)) {
logger.info("图片验证码错误!");
throw new DisabledException("图形验证码错误!");
}
logger.info("验证码验证成功");
String username = authentication.getName();
password = (String) authentication.getCredentials();
user = authUserDetailsService.loadUserByUsername(username);
//加密过程在这里体现
logger.info("结果CustomUserDetailsService后,已经查询出来的数据库存储密码:" + user.getPassword());
if (!passwordEncoder.matches(password, user.getPassword())) {
logger.info("登录用户密码错误!");
throw new DisabledException("登录用户密码错误!");
}
}else {//使用用户token进行登录
String username = authentication.getName();
password = (String) authentication.getCredentials();
user = authUserDetailsService.loadUserByUsername(username);
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
@Override
public boolean supports(Class<?> arg0) {
return true;
}
}
KaptchaConfig:验证码配置类
/**
* 验证码配置类
*/
@Component
public class KaptchaConfig {
@Bean
public DefaultKaptcha getDefaultKaptcha() {
com.google.code.kaptcha.impl.DefaultKaptcha defaultKaptcha = new com.google.code.kaptcha.impl.DefaultKaptcha();
Properties properties = new Properties();
// 图片边框
properties.setProperty("kaptcha.border", "no");
// 边框颜色
properties.setProperty("kaptcha.border.color", "black");
//边框厚度
properties.setProperty("kaptcha.border.thickness", "1");
// 图片宽
properties.setProperty("kaptcha.image.width", "200");
// 图片高
properties.setProperty("kaptcha.image.height", "50");
//图片实现类
properties.setProperty("kaptcha.producer.impl", "com.google.code.kaptcha.impl.DefaultKaptcha");
//文本实现类
properties.setProperty("kaptcha.textproducer.impl", "com.google.code.kaptcha.text.impl.DefaultTextCreator");
//文本集合,验证码值从此集合中获取
properties.setProperty("kaptcha.textproducer.char.string", "01234567890");
//验证码长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字体
properties.setProperty("kaptcha.textproducer.font.names", "宋体");
//字体颜色
properties.setProperty("kaptcha.textproducer.font.color", "black");
//文字间隔
properties.setProperty("kaptcha.textproducer.char.space", "5");
//干扰实现类
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
//干扰颜色
properties.setProperty("kaptcha.noise.color", "blue");
//干扰图片样式
properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
//背景实现类
properties.setProperty("kaptcha.background.impl", "com.google.code.kaptcha.impl.DefaultBackground");
//背景颜色渐变,结束颜色
properties.setProperty("kaptcha.background.clear.to", "white");
//文字渲染器
properties.setProperty("kaptcha.word.impl", "com.google.code.kaptcha.text.impl.DefaultWordRenderer");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
生成验证码工具类
public class CommonUtil {
/**
* 生成验证码图片
* @param request 设置session
* @param response 转成图片
* @param captchaProducer 生成图片方法类
* @param validateSessionKey session名称
* @throws Exception
*/
public static void validateCode(HttpServletRequest request, HttpServletResponse response, DefaultKaptcha captchaProducer, String validateSessionKey) throws Exception{
// Set to expire far in the past.
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
// create the text for the image
String capText = captchaProducer.createText();
// store the text in the session
request.getSession().setAttribute(validateSessionKey, capText);
// create the image with the text
BufferedImage bi = captchaProducer.createImage(capText);
ServletOutputStream out = response.getOutputStream();
// write the data out
ImageIO.write(bi, "jpg", out);
try {
out.flush();
} finally {
out.close();
}
}
}
Controller层验证码
@PostMapping(value = {"/loginValidateCode"})
@ApiOperation(value = "获取验证码图片")
public void loginValidateCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.info("start UserController.loginValidateCode");
CommonUtil.validateCode(request, response, captchaProducer, LOGIN_VALIDATE_CODE);
logger.info("end UserController.loginValidateCode");
}
验证码部分自测
响应结果的封装
MyAuthenctiationDeniedHandler:
/**
* 无权访问
*/
@Component
public class MyAuthenctiationDeniedHandler implements AccessDeniedHandler {
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
logger.info("无权访问!");
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("403");
responseBody.setMsg("Need Authorities!");
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
MyAuthenctiationEntryPointHandler
/**
* 未登录
*
*/
@Component
public class MyAuthenctiationEntryPointHandler implements AuthenticationEntryPoint{
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.info("未登录!");
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("402");
responseBody.setMsg("Need Login!");
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
MyAuthenctiationFailureHandler
/**
* 登录失败
*
*/
@Component("myAuthenctiationFailureHandler")
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败!");
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("401");
responseBody.setMsg("Login Failure!");
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
MyAuthenctiationInvalidSessionStrategy
/**
* session到期
*
*/
@Component
public class MyAuthenctiationInvalidSessionStrategy implements InvalidSessionStrategy{
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
logger.info("session到期!");
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("101");
responseBody.setMsg("Session Expires!");
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
MyAuthenctiationLogoutSuccessHandler
/**
* 退出登录
*
*/
@Component
public class MyAuthenctiationLogoutSuccessHandler implements LogoutSuccessHandler{
@Autowired
RedisTemplate redisTemplate;
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
logger.info("退出登录!");
String username = authentication.getName();
//删除用户令牌
String token =redisTemplate.opsForValue().get("token_"+username).toString();
redisTemplate.delete("token_"+username);
redisTemplate.delete("token_"+token);
request.getSession().setAttribute("token","");
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("200");
responseBody.setMsg("Logout Success!");
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
MyAuthenctiationSessionInformationExpiredStrategy
/**
* session到期,被登录
*/
@Component
public class MyAuthenctiationSessionInformationExpiredStrategy implements InvalidSessionStrategy {
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
logger.info("session到期,被登录!");
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("102");
responseBody.setMsg("Session Expires!");
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
MyAuthenctiationSuccessHandler
/**
* 登录成功
*
*/
@Component("myAuthenctiationSuccessHandler")
public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
RedisTemplate redisTemplate;
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登录成功!");
String token =null;
if(request.getSession().getAttribute("token")!=null){
token = request.getSession().getAttribute("token").toString();
}
if(StringUtils.isNotBlank(token)){
//生成uuid作为用户token
String tokentemp = CommonUtil.getUUID();
String username = authentication.getName();
//将用户令牌存在redis中 生命周期为2个小时
redisTemplate.opsForValue().set("token_"+username,tokentemp,7200, TimeUnit.SECONDS);
redisTemplate.opsForValue().set("token_"+tokentemp,username,7200, TimeUnit.SECONDS);
//将用户token放在cookie中
CookieUtil.set(response, "token",tokentemp,7200);
}
ResponseBody responseBody = new ResponseBody();
responseBody.setStatus("200");
responseBody.setMsg("Login Success!");
responseBody.setResult(token);
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
验证
1.使用密码进行登录
2.使用token进行登录
推荐阅读
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之九如何进行用户权限控制
-
基于Spring Security前后端分离的权限控制系统问题
-
spring boot环境下实现restful+前后端分离的网页开发
-
Spring Security 前后端分离下的权限控制
-
Springboot + Spring Security 前后端分离权限控制
-
前后端分离下spring security 跨域问题等
-
jwt,spring security ,feign,zuul,eureka 前后端分离 整合 实现 简单 权限管理系统 与 用户认证的实现
-
Springboot+Spring Security实现前后端分离登录认证及权限控制的示例代码
-
解析spring-security权限控制和校验的问题
-
前后端分离模式下的权限设计方案