SpringSecurity在前后端分离中的应用

框架简介

​ Spring Security是基于Spring AOP和Servlet过滤器的安全框架,它提供全面的安全性解决方案。同时在web请求级和方法调用级处理身份确认和授权。

项目介绍

​ 在不分离情况下使用过了SpringSecurity,感觉还是非常不错的,所以想在前端后端分离的情况下,玩一把SpringSecurity,其中遇到了不少问题,不过在度娘的支持下成功了。特此写了这篇随笔,如果文中存在一些问题和有更好的写法请告诉我,小僧万分感谢。

​ 项目用到的技术:SpringBoot +SpringSecurity +vue

具体实现

加入依赖

​ 这里就直接把我的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.4.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.zzgk</groupId>
	<artifactId>mas</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sz-java</name>
	<description>mas project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-rest</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.46</version>
        </dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.6</version>
		</dependency>
    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

jwt配置

​ 前后端分离项目,在登录时session是用不了的,所以要使用jwt(不知道jwt可以百度一下)来完成登录鉴权。

配置私钥,过期时间,名称

​ 在application.yml加入私钥,token过期时间,名称配置,refreshToken的过期时间,这里需要解释一下refreshToken是当token过期时,通过请求来刷新token的,当refreshToken过期则判断用户不活跃,这时则需要重新登录。

jwt:
  secret: ffjaksdfjak #私钥 自己可以自由发挥
  expiration: 3600000 #过期时间 半个小时
  header:  JWTHeaderName #自己往请求头部放的名字
  refresh_expiration: 651000000 #刷新token的过期时间7天
编写jwt工具类

​ 配置完成后,需要编写一个类,用于生成jwt,验证令牌,判断jwt是否过期等,

package com.zzgk.sys.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JsonWebTokenUtil {
    private String secret;
    private Long expiration;
    private String header;
    private Long refresh_expiration;

    /**
     * 生成令牌
     * @return
     */
    public String generateToken(UserDetails details,String roles){
        Map<String,Object> claims = new HashMap<>();
        claims.put("sub", details.getUsername());
        claims.put("create",new Date());
        claims.put("role",roles);
        return generateToken(claims);
    }
    /**
     * 从claims生成令牌
     * @param claims
     * @return
     */
    private String generateToken(Map<String,Object> claims){
        Date expirationDate=new Date(System.currentTimeMillis()+expiration);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)//设置过期时间
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     *
     * @param details 用户信息
     * @param roles 用户角色
     * @return 生成RefreshToken
     */
    public String generateRefreshToken(UserDetails details, String roles){
        Map<String,Object> claims = new HashMap<>();
        claims.put("sub", details.getUsername());
        claims.put("create",new Date());
        claims.put("role",roles);
        //设置过期时间为七天;
        Date expirationDate = new Date(System.currentTimeMillis()+refresh_expiration);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 从令牌中获取用户名
     * @param token 令牌
     * @return 用户名
     */
    public  String getUsernameFromToken(String token){
        String username;
        try {
            Claims claims=getClaimsFromToken(token);
            username=claims.getSubject();
        }catch (ExpiredJwtException e){
            username=null;
        }
        return username;
    }

    /**
     * 无视过期时间,获取用户名
     * @param token
     * @return
     */
    public  String getUsernameIgnoreExpiration(String token){
        String username;
        try {
            Claims claims=getClaimsFromToken(token);
            username=claims.getSubject();
        }catch (ExpiredJwtException e){
            username=e.getClaims().getSubject();
        }
        return username;
    }
    /**
     * 从令牌中获取数据声明,
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token){
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    private Boolean isTokenExpired(String token){
        try{
            Claims claims=getClaimsFromToken(token);
            Date expiration=claims.getExpiration();
            return expiration.before(new Date());
        }catch (Exception e){
            return  false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token){
        String refreshedToken;
        try {
            Claims claims=getClaimsFromToken(token);
            claims.put("created",new Date());
            refreshedToken=generateToken(claims);
        }catch (ExpiredJwtException e){
            Claims claims = e.getClaims();
            claims.put("created",new Date());
            refreshedToken= generateToken(claims);
        }
        return  refreshedToken;
    }

    /**
     * 验证令牌
     * @param token 令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token,UserDetails userDetails){
        String username=getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername())&&!isTokenExpired(token));
    }
}

Spring Security配置

一、起步

​ 编写一个类(我起名为MasSecurity) 让这个类继承WebSecurityConfigurerAdapter,同时添加@EnableWebSecurity注解,重写configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法

@EnableWebSecurity
public class MasSecurity extends WebSecurityConfigurerAdapter{
     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      …… 
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      …… 
    }
}

​ 这一块的配置还是蛮多的,在配置的时候也花了不少时间。因为项目是前后端分离的,这样就涉及到了跨域问题,首先我们先来解决跨域问题。

二、跨域配置

​ 在configure(HttpSecurity http)方法中添加如下配置,允许跨域后SpringSecurity会自动寻找名字为corsConfigurationSource的Bean,使用该bean的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.cors();//允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
	http.csrf().disable();//关闭CSRF防御
}

​ 那么接下来,我们只需要写一个corsConfigurationSource的bean,来设置跨域相关的配置

package com.zzgk.sys.config;

import com.zzgk.sys.util.JsonWebTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 跨域配置
 * @author cpms
 */
@Configuration
public class CrossDomainConfig {

    @Autowired
    JsonWebTokenUtil jsonWebTokenUtil;

    /**
     *
     * @return 基于URL的跨域配置信息
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration cores=new CorsConfiguration();
        cores.setAllowCredentials(true);//允许客户端携带认证信息
        //springBoot 2.4.1版本之后,不可以用 * 号设置允许的Origin,如果不降低版本,则在跨域设置时使用setAllowedOriginPatterns方法
       // cores.setAllowedOrigins(Collections.singletonList("*"));//允许所有域名可以跨域访问
        cores.setAllowedOriginPatterns(Collections.singletonList("*"));
        cores.setAllowedMethods(Arrays.asList("GET","POST","DELETE","PUT","UPDATE"));//允许哪些请求方式可以访问
        cores.setAllowedHeaders(Collections.singletonList("*"));//允许服务端访问的客户端请求头
        // 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
        cores.addExposedHeader(jsonWebTokenUtil.getHeader());
        // 注册跨域配置
        // 也可以使用CorsConfiguration 类的 applyPermitDefaultValues()方法使用默认配置
        source.registerCorsConfiguration("/**",cores.applyPermitDefaultValues());
        return source;
    }

}

这样跨域配置设置完毕

三、登录配置

​ 登录的配置还是比较多的,首先我们要写一个service让其实现UserDetailsService接口,重写loadUserByUsername方法

@Service
public class UserServiceImpl implements UserDetailsService {
	@Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserEntity user = userDao.findByUserName(s);
        if (user!=null){
            SysUserDetail detail = new SysUserDetail();
            detail.setId(user.getId());
            detail.setUsername(user.getUsername());
            detail.setPassword(user.getPassword());
            AccountState accountState = accountStateDao.getAccountSateByUserid(user.getId());
            detail.setAccountNonExpired(accountState.getAccountNonExpired() == 1);
            detail.setAccountNonLocked(accountState.getAccountNonLocked()==1);
            detail.setEnabled(accountState.getEnabled()==1);
            detail.setCredentialsNonExpired(accountState.getCredentialsNonExpired()==1);
            //查询用户权限
            List<GrantedAuthority> authorities =new ArrayList<>();
            List<Map<String,Object>> roles=roleDao.getRoleList(user.getId());
            for (Map<String,Object> one:roles) {
                SimpleGrantedAuthority authority=new SimpleGrantedAuthority((String) one.get("code"));
                authorities.add(authority);
            }
            detail.setAuthorities(authorities);
            return detail;
        }else
        throw new UsernameNotFoundException("该账号不存在");
    }
}

​ 实现一个自定义的登录处理,编写一个类实现AuthenticationProvider接口

package com.zzgk.sys.config;
import com.zzgk.sys.serviceimpl.sys.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
/**
 * 登录处理,
 */
@Component
public class LoginAuthProvider implements AuthenticationProvider {

    @Autowired
    UserServiceImpl userServiceImpl;

    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        //获取用户名和密码
        String username = auth.getName();
        String password = (String) auth.getCredentials();
        UserDetails userDetail  = userServiceImpl.loadUserByUsername(username);
        if (!userDetail.isEnabled()){
            throw new DisabledException("该账号已禁用,请联系管理员");
        }else if (!userDetail.isAccountNonExpired()){
            throw new AccountExpiredException("该账号已过期,请联系管理员");
        }else if(!userDetail.isAccountNonLocked()){
            throw new LockedException("该账号已被锁定,请联系管理员");
        }else if(!userDetail.isCredentialsNonExpired()){
            throw new CredentialsExpiredException("该账号的登录凭证已过期,请重新登录");
        }
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        if(!passwordEncoder.matches(password,userDetail.getPassword())){
            throw  new BadCredentialsException("密码错误请重新输入");
        }
        return new UsernamePasswordAuthenticationToken(userDetail,password,userDetail.getAuthorities());
    }

    // supports函数用来指明该Provider是否适用于该类型的认证,如果不合适,则寻找另一个Provider进行验证处理。
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

​ 将自定义处理加入Spring Security中,在configure(AuthenticationManagerBuilder auth)中加入该方法。

@EnableWebSecurity
public class MasSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginAuthProvider loginAuthProvider; 	

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //登录处理
        auth.authenticationProvider(loginAuthProvider);
    }
}    

​ 继续在configure(HttpSecurity http)方法中添加一些配置。

protected void configure(HttpSecurity http) throws Exception {
        //允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
        http.cors();
        http.csrf().disable();
        //当访问接口失败的配置
        http.exceptionHandling().authenticationEntryPoint(new InterfaceAccessException());
        http.authorizeRequests()
                .antMatchers("/login","/refreshToken","/user/check_login").permitAll()//设置哪些方法允许匿名访问
                .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")//该方法需要管理员角色才能访问
                .anyRequest().authenticated()//其他方法都需要登录才能访问
                .and()
                .formLogin()//选用formLogin模式
                .successHandler(new MySuccessHandler())//登录成功的处理
                .failureHandler(new MyFailHandler())//登录失败的处理
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//因为用不到session,所以选择禁用
        //向过滤器链中添加,自定义的jwt过滤器和json过滤器
        //在UsernamePasswordAuthenticationFilter之前添加jwtAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        //在UsernamePasswordAuthenticationFilter之后添加jsonAuthenticationFilter
            .addFilterAfter(getJsonAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
    }

​ 登录成功的处理类

	//登录成功的处理类
     class MySuccessHandler implements AuthenticationSuccessHandler{

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {
            UserDetails details = (UserDetails) authentication.getPrincipal();
            List<GrantedAuthority> roles = (List<GrantedAuthority>) details.getAuthorities();
            //登录时同时生成refreshToken,保存到表中
            RefreshToken token = new RefreshToken();
            token.setUsername(details.getUsername());
            String refreshToken = jsonWebTokenUtil.generateRefreshToken(details,roles.get(0).getAuthority());
            token.setToken(refreshToken);
            //如果存在则更新
            if (refreshTokenDao.existRefreshToken(details.getUsername())>0){
                refreshTokenDao.updateRefreshToken(token);
            }else {
                refreshTokenDao.save(token);
            }
            //将jwt返回
            response.setHeader(jsonWebTokenUtil.getHeader(), jsonWebTokenUtil.generateToken(details,roles.get(0).getAuthority()));
            //返回前端
            ResponseMsgUtil.sendSuccessMsg("成功",null,response);

        }
    }

​ 登录失败的处理类

//登录失败的处理
    static class MyFailHandler implements AuthenticationFailureHandler{

        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                            AuthenticationException e) throws IOException, ServletException {
          	//获取错误信息返回前台
            ResponseMsgUtil.sendFailMsg(e.getMessage(),response);
        }
    }

jwtAuthenticationFilter类,该类主要作用于从前端请求头部解析出jwt,检查是否登录。

package com.zzgk.sys.config;

import com.zzgk.sys.serviceimpl.sys.UserServiceImpl;
import com.zzgk.sys.util.JsonWebTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
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;

/**
 * 登录成功后,此类进行鉴权,
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    JsonWebTokenUtil tokenUtil;
    @Autowired
    UserServiceImpl userServiceImpl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //从请求头部获取json web token
        String jwt = request.getHeader(tokenUtil.getHeader());
        //判断是否不为空
        if (StringUtils.hasLength(jwt)&&!jwt.equals("null")&&!jwt.equals("undefined")) {
            //从jwt中获取用户名
            String username = tokenUtil.getUsernameFromToken(jwt);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                //通过用户名查询
                UserDetails userDetails = userServiceImpl.loadUserByUsername(username);
                //创建认证信息
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username,
                        userDetails.getPassword(), userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request,response);
    }
}

​ getJsonAuthenticationFilter()方法

 @Bean
    JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

JsonAuthenticationFilter类,实现了从json获取用户名和密码

package com.zzgk.sys.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * 默认的用户名/密码提取是通过request中的getParameter来提取的
 * 该过滤器实现了从json获取用户名和密码
 * @author cpms
 */
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //判断请求类型是否是json
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authenticationToken = null;
            try {
                InputStream is = request.getInputStream();
                Map<String,String> authenticationBean = mapper.readValue(is,Map.class);
                authenticationToken = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"),
                        authenticationBean.get("password"));
            }catch (IOException e){
                e.printStackTrace();
                authenticationToken = new UsernamePasswordAuthenticationToken("","");
            }
            setDetails(request,authenticationToken);
            return this.getAuthenticationManager().authenticate(authenticationToken);
        }else {
            return super.attemptAuthentication(request, response);
        }

    }
}

​ InterfaceAccessException类,用于处理接口访问时身份过期

package com.zzgk.sys.config;

import com.zzgk.sys.util.ResponseMsgUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class InterfaceAccessException implements AuthenticationEntryPoint {



    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {

        if(e.getMessage().equals("Full authentication is required to access this resource")){
            ResponseMsgUtil.send401Msg("登录超时,请重新登录",response);
        }else {
            ResponseMsgUtil.sendFailMsg(e.getMessage(),response);
        }
    }

    public boolean isAjaxRequest(HttpServletRequest request) {
        String ajaxFlag = request.getHeader("X-Requested-With");
        return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag);
    }
}

​ MasSecurity的全部配置

package com.zzgk.sys.config;
import com.zzgk.sys.dao.sys.RefreshTokenDao;
import com.zzgk.sys.entity.sys.RefreshToken;
import com.zzgk.sys.util.JsonWebTokenUtil;
import com.zzgk.sys.util.ResponseMsgUtil;
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.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.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;


/**
 * spring security 配置类
 * @author cpms
 */
@EnableWebSecurity
public class MasSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginAuthProvider loginAuthProvider;
    @Autowired
    JwtAuthenticationFilter jwtAuthenticationFilter;
    @Autowired
    JsonWebTokenUtil jsonWebTokenUtil;
    @Autowired
    RefreshTokenDao refreshTokenDao;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //登录处理
        auth.authenticationProvider(loginAuthProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
        http.cors();
        http.csrf().disable();
        //当访问接口失败的配置
        http.exceptionHandling().authenticationEntryPoint(new InterfaceAccessException());
        http.authorizeRequests()
                .antMatchers("/login","/refreshToken","/user/check_login").permitAll()
                .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(new MySuccessHandler())//登录成功的处理
                .failureHandler(new MyFailHandler())//登录失败的处理
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//因为用不到session,所以选择禁用
        //向过滤器链中添加,自定义的jwt过滤器和json过滤器
        //在UsernamePasswordAuthenticationFilter之前添加jwtAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        //在UsernamePasswordAuthenticationFilter之后添加jsonAuthenticationFilter
            .addFilterAfter(getJsonAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
    }

    //登录成功的处理类
     class MySuccessHandler implements AuthenticationSuccessHandler{

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {

            UserDetails details = (UserDetails) authentication.getPrincipal();
            List<GrantedAuthority> roles = (List<GrantedAuthority>) details.getAuthorities();
            //登录时同时生成refreshToken,保存到表中
            RefreshToken token = new RefreshToken();
            token.setUsername(details.getUsername());
            String refreshToken = jsonWebTokenUtil.generateRefreshToken(details,roles.get(0).getAuthority());
            token.setToken(refreshToken);
            if (refreshTokenDao.existRefreshToken(details.getUsername())>0){
                refreshTokenDao.updateRefreshToken(token);
            }else {
                refreshTokenDao.save(token);
            }
            response.setHeader(jsonWebTokenUtil.getHeader(), jsonWebTokenUtil.generateToken(details,roles.get(0).getAuthority()));
            ResponseMsgUtil.sendSuccessMsg("成功",null,response);

        }
    }

    //登录失败的处理
    static class MyFailHandler implements AuthenticationFailureHandler{

        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                            AuthenticationException e) throws IOException, ServletException {
           ResponseMsgUtil.sendFailMsg(e.getMessage(),response);
        }
    }

    @Bean
    JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

}

vue配置

保存jwt
 localStorage.setItem("jwt",res.headers.jwtheadername);
axios配置
import axios from 'axios'
import app from '../main'
import store from '../vuex/store'
import qs from 'qs'

//获取jwt
function getLocalToken() {
    const token = localStorage.getItem("jwt");
    return token;
}

//保存jwt
function saveToken(obj) {
    localStorage.setItem("jwt", obj);
}
function clearToken(){
    localStorage.removeItem("jwt");
}
//刷新jwt
function refreshToken() {
    return axios.post("/refreshToken");
}
//验证jwt
function validateToken() {
    return axios.post("/validateToken");
}

async function validateRefreshToken(c){
    let valid = await validateToken().then(res=>{
        return res.data.state=='ok' 
    });
    if(!valid){
        if (!isRefreshing) {
            config.log("刷新token中");
            isRefreshing = true;
            refreshToken().then(res => {
                //刷新失败
                if (res.data.state == 'fail') {
                    //如果失败就清空队列,防止多次接口无效访问。
                    requests=[];
                    throw res.data.msg;
                }else{
                    const jwt = res.data.data.jwt;
                    saveToken(jwt);
                    isRefreshing = false;
                    return token;
                }
            }).then((token) => {
                requests.forEach(cb => cb(token));
                //执行完成后,清空队列
                requests = [];
            }).catch(error => {
                console.log(error);
            });
        }
        const retryOriginalRequst = new Promise((resolve) => {
            requests.push((token) => {
                config.headers['JWTHeaderName'] = token;
                resolve(config);
            })
        })
        return retryOriginalRequest;
    }
    return c;
}

// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
axios.defaults.baseURL = "http://localhost:8700";


//是否正在刷新的标记
let isRefreshing = false;

//重试队列,每一项将是一个待执行的函数形式
let requests = []

//request拦截器,每次请求前从cookie中获取jwt,加入请求头部
axios.interceptors.request.use(
    config => {
        let jwt = getLocalToken();
       //console.log(localStorage);
        config.headers['JWTHeaderName'] = jwt;
        //登录接口、刷新token接口、检查登录接口直接跳过验证
        if (config.url.indexOf("/login") >= 0 || config.url.indexOf("/refreshToken") >= 0
            || config.url.indexOf("/user/check_login") >= 0 || config.url.indexOf('/validateToken') >= 0) {
            //let jwt =  store.state.jwt;
            return config;
        }
        return validateRefreshToken(config);
    },
    error => {
        Promise.reject(error);
    }
)

//response拦截器
axios.interceptors.response.use(
    response => {
        const resp = response.data;
        if(resp.code=='200'){
            return response;
        }else if(resp.code=='401'){
            app.$message("登录过期,请重新登录");
            window.location.href = '/';        
        } else{
            console.log(response)
            return Promise.reject(resp.msg)
        }
    },
    error => {
        if (error && error.response) {
            switch (error.response.status) {
                case 400:
                    error.message = '错误请求';
                    break;
                case 401:
                    error.message = '请重新登录';
                    break;
                case 403:
                    error.message = "拒绝访问";
                    break;
                case 404:
                    error.message = '请求错误,未找到该资源';
                    break;
                case 405:
                    error.message = '请求方法未允许';
                    break;
                case 408:
                    error.message = '请求超时';
                    break;
                case 500:
                    error.message = '服务器端出错';
                    break;
                case 501:
                    error.message = '网络未实现';
                    break;
                case 502:
                    error.message = '网络错误';
                    break;
                case 503:
                    error.message = '服务不可用';
                    break;
                case 504:
                    error.message = '网络超时';
                    break;
                case 505:
                    error.message = 'http版本不支持该请求';
                    break;
                default:
                    error.message = `未知错误${error.response.status}`;

            }
        } else {
            error.message = "连接到服务器失败";
        }
        return Promise.reject(error.message);
    }
)


export default {
    serverUrl: "http://localhost:8700",
    post: function (url, data, config) {
        if (!data)
            data = {};
        if (!config)
            config = {};
        config.header = {};
        if (data instanceof FormData) {
            if (!config.header)
                config.header = {};
            config.header['content-type'] = 'multipart/form-data';
            return axios.post(url, data, config);
        }
        return axios.post(url, qs.stringify(data, { arrayFormat: 'repeat' }), config);
    },
    get: function (url, config) {
        if (!config)
            config = {};
        config.withCredentials = true;
        return axios.get(url, config);
    },
    put: function (url, data, config) {
        if (!data) {
            data = {};
        }
        if (!config) {
            config = {};
        }
        config.header = {}
        return axios.put(url, qs.stringify(data, { arrayFormat: 'repeat' }), config);
    },
    delete(url, data, config){
        if (!data) {
            data = {};
        }
        if (!config) {
            config = {};
        }
        config.header = {}
        return axios.delete(url, qs.stringify(data, { arrayFormat: 'repeat' }), config)
    },
    getLocalToken,
    validateToken,
    clearToken,
}
router配置
function existToken(){
  return new Promise((resolve, reject)=>{
      let token = global.getLocalToken();
      resolve(token!=null&&token.trim().length>0);
  })
}


// 每次跳转页面前,检查是否已经登录
router.beforeEach((to, from, next) => {
  //当跳转页时login时清空标签栏
  if (to.name == "login") {
    //如果已含有jwt且没过期就直接去index
    let  exist  = existToken();
    if(exist){
      global.post("/user/check_login").then(function (res, req) {
        if (res.data.state == "ok") {
          if (res.data.data){
            localStorage.setItem("jwt", res.data.data);
          }
          next({name:'index'});
        } 
      }).catch(error => {
        console.log();
      });
    }
  }
  if (to.meta.requireAuth) {
    //const now = Date.now();
    //const exp = localStorage.getItem("exp");
    global.post("/user/check_login").then(function (res, req) {
      if (res.data.state == "ok") {
        if (res.data.data){
          localStorage.setItem("jwt", res.data.data);
        }
        next();
      } else {
        //store.dispatch('clearTab');
        app.$message(res.data.msg);
        next({ name: 'login' });
      }
    }).catch(error => {
      if (typeof error != 'undefined') {
        app.$message(error);
      }
      next({ name: 'login' });
    });
  } else {
    next()
  }

});

github地址

vue:https://github.com/FengQingZhang/sz-vue

java:https://github.com/FengQingZhang/sz-java

结语

如果您发现文章中有哪些问题,请联系我并指导我改正,或者您有更好的写法,麻烦您指点一二,本人万分感谢,
文中vue部分参考了https://zhuanlan.zhihu.com/p/80125501这表文章,对作者表示感谢。
  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
毕业设计是高等教育阶段学生在完成学业前所进行的一项重要学术任务,旨在检验学生通过学习所获得的知识、技能以及对特定领域的深刻理解能力。这项任务通常要求学生运用所学专业知识,通过独立研究和创新,完成一个实际问题的解决方案或者开展一项有价值的项目。 首先,毕业设计的选择通常由学生根据个人兴趣、专业方向以及实际需求来确定。学生需要在导师的指导下明确研究目标、问题陈述,确立研究的范围和深度。毕业设计可以包括文献综述、需求分析、方案设计、实施与测试等多个阶段,以确保整个过程的科学性和系统性。 其次,毕业设计的完成通常需要学生具备一定的独立思考和解决问题的能力。在研究过程,学生可能需要采用各种研究方法,如实验、调查、案例分析等,以获取必要的数据和信息。通过这些活动,学生能够培养扎实的专业技能,提升解决实际问题的实际能力。 第三,毕业设计的撰写是整个过程的重要组成部分。学生需要将研究过程、方法、结果以及结论等详细记录在毕业论文,以展示其研究的全貌和成果。同时,撰写毕业设计还有助于提高学生的学术写作水平,培养清晰、逻辑的表达能力。 最后,毕业设计的评价通常由导师和相关专业人士进行。评价标准包括研究的创新性、实用性、方法的科学性以及论文的质量等方面。学生在毕业设计获得的成绩也将直接影响其最终的学业成绩和学位授予。 总的来说,毕业设计是高等教育的一项重要环节,通过此过程,学生不仅能够巩固所学知识,还能培养独立思考和解决问题的能力,为将来的职业发展奠定坚实的基础。
基于SpringBoot,Spring Security,JWT,Vue & Element 的,目前主要模块划分为门诊部分,住院登记收费、住院医生护士站三个部分。 毕业设计是高等教育阶段学生在完成学业前所进行的一项重要学术任务,旨在检验学生通过学习所获得的知识、技能以及对特定领域的深刻理解能力。这项任务通常要求学生运用所学专业知识,通过独立研究和创新,完成一个实际问题的解决方案或者开展一项有价值的项目。 首先,毕业设计的选择通常由学生根据个人兴趣、专业方向以及实际需求来确定。学生需要在导师的指导下明确研究目标、问题陈述,确立研究的范围和深度。毕业设计可以包括文献综述、需求分析、方案设计、实施与测试等多个阶段,以确保整个过程的科学性和系统性。 其次,毕业设计的完成通常需要学生具备一定的独立思考和解决问题的能力。在研究过程,学生可能需要采用各种研究方法,如实验、调查、案例分析等,以获取必要的数据和信息。通过这些活动,学生能够培养扎实的专业技能,提升解决实际问题的实际能力。 第三,毕业设计的撰写是整个过程的重要组成部分。学生需要将研究过程、方法、结果以及结论等详细记录在毕业论文,以展示其研究的全貌和成果。同时,撰写毕业设计还有助于提高学生的学术写作水平,培养清晰、逻辑的表达能力。 最后,毕业设计的评价通常由导师和相关专业人士进行。评价标准包括研究的创新性、实用性、方法的科学性以及论文的质量等方面。学生在毕业设计获得的成绩也将直接影响其最终的学业成绩和学位授予。 总的来说,毕业设计是高等教育的一项重要环节,通过此过程,学生不仅能够巩固所学知识,还能培养独立思考和解决问题的能力,为将来的职业发展奠定坚实的基础。
Spring Security是一个功能强大的身份验证和访问控制架,可以用于保护Java应用程序的安全性。在前后端分离的项目Spring Security可以用于实现用户认证和授权功能。 下面是Spring Security整合前后端分离项目的一般步骤: 1. 添加依赖:在项目的构建文件添加Spring Security的依赖,例如使用Maven的话,在pom.xml文件添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 2. 配置Spring Security:创建一个配置类,继承自`WebSecurityConfigurerAdapter`,并重写`configure`方法来配置Spring Security的行为。在这个方法,你可以定义用户认证、授权规则以及其他安全相关的配置。 3. 用户认证:可以使用Spring Security提供的默认用户认证方式,也可以自定义用户认证逻辑。如果使用默认方式,可以在配置类通过`configure`方法配置一个内存的用户存储,或者使用数据库存储用户信息。 4. 授权规则:通过`configure`方法配置URL路径的访问权限,可以设置哪些URL需要进行认证,哪些URL不需要认证。可以使用`antMatchers`方法来匹配URL路径,并设置相应的访问权限。 5. 跨域资源共享(CORS):由于前后端分离项目,前端和后端可能运行在不同的域名下,需要配置CORS来允许跨域请求。可以通过`configure`方法配置CORS相关的设置。 6. 登录和注销:在前后端分离项目,一般使用JSON Web Token(JWT)来实现无状态的认证。可以通过`configure`方法配置登录和注销的URL,并定义相应的处理逻辑。 7. 异常处理:在配置类可以定义异常处理的逻辑,例如处理认证失败、访问拒绝等异常情况。 8. 前端集成:前端需要发送认证请求,并在每个请求携带认证信息。可以使用前端框架(如Vue、React等)来发送请求,并将认证信息存储在本地(如LocalStorage)。 以上是Spring Security整合前后端分离项目的一般步骤,具体的实现方式会根据项目需求和技术栈的不同而有所差异。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值