目录
准备内容
新建项目,SpringBoot采用2.7.11,spring-boot-starter-security,vue 2.0
默认会自动启动拦截装置,可以这样取消,先这样配置一下
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
JWT 校验用户的凭证(头部,载荷,签证)
引入依赖
<!-- JWT --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency>
逻辑:用户登录后,请求头中加一个token参数,如果有则表示可以访问,没有就不能访问,这是一个小测试还是有点缺陷的
SpringSecurity 权限控制
一共有两个最主要拦截器
登陆验证拦截器AuthenticationProcessingFilter
资源管理拦截器AbstractSecurityInterceptor
但拦截器里面的实现需要一些组件来实现分别是
AuthenticationManager认证管理器
accessDecisionManager决策管理器等组件来支撑。
流程图解读:
用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实 现类。
然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权 限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication
通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出 AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实 现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道 web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着 一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至 Authentication。
现在关掉之前的SecurityAutoConfiguration.class,打开是不是直接跳转到了登录页面呢,无法访问接口了
配置 SpringSecurity
先说下创建token的工具类
/** * 签发JWT * * @param id 用户ID * @param subject 可以是JSON数据 尽可能少 * @param ttlMillis 不知道 * @return 结果 */ public static String createJWT(String id, String subject, long ttlMillis) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 获取当前时间毫秒 long nowMillis = System.currentTimeMillis(); // 获取当前时间日期类型 Date now = new Date(nowMillis); // 生成一个加密的key SecretKey secretKey = generalKey(); JwtBuilder builder = Jwts.builder() .setId(id) .setSubject(subject) // 主题 .setIssuer("ChiHaiKeJi2016") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙 if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); builder.setExpiration(expDate); // 过期时间 } return builder.compact(); } /** * 生成jwt token * @param username 用户名 */ public static String genJwtToken(String username) { return createJWT(username, username, 60 * 60 * 1000); } /** * 生成加密Key */ public static SecretKey generalKey() { // 目前只知道这个方法生成一个key,并用base64解码 byte[] encodedKey = Base64.decode(JwtConstant.JWT_SECERT); // 目前不知道啥意思 return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); }
配置信息--新建SecurityConfig
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { // 白名单 private static final String[] URL_WHITE_LIST = {"/login"}; @Resource private LoginHandler loginHandler; // 密码加密方式 @Bean protected PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { // 开启跨域 以及csrf攻击关闭 httpSecurity.cors().and().csrf().disable() // 登录登出配置 .formLogin() // 登录成功接口 .successHandler(loginHandler) // 登录失败的接口 .failureHandler(loginHandler) // .and().logout().logoutSuccessHandler() // session禁用配置 .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,前后端分离 // 拦截规则配置 // // 白名单内容放行 .and().authorizeRequests().antMatchers(URL_WHITE_LIST).permitAll() // 需要认证 .anyRequest().authenticated(); return httpSecurity.build(); } }
配置登录失败与成功接口---新建---LoginHandler 处理类
/** * 登录成功与失败处理器 */ @Component public class LoginHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler { // 登录成功的 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); ServletOutputStream stream = response.getOutputStream(); // 获取token String token = JwtUtils.genJwtToken("user"); // 写入数据 stream.write(JSONUtil.toJsonStr(new Result("登录成功", token)).getBytes()); stream.flush(); stream.close(); } // 登录失败的 @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); ServletOutputStream stream = response.getOutputStream(); String message = exception.getMessage(); if (exception instanceof BadCredentialsException) { message = "用户名或者密码错误!"; } stream.write(JSONUtil.toJsonStr(new Result(message)).getBytes(StandardCharsets.UTF_8)); stream.flush(); stream.close(); } }
在用户service实现类中自定义查询数据库
@Service public class SysUserServiceImpl implements SysUserService, UserDetailsService { @Resource private SysUserMapper sysUserMapper; // 自定义查询参数是用户名 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<SysUser> sysUserLambdaQueryWrapper = new LambdaQueryWrapper<>(); sysUserLambdaQueryWrapper.eq(SysUser::getUsername, username); SysUser user = sysUserMapper.selectOne(sysUserLambdaQueryWrapper); if (user == null) { throw new UsernameNotFoundException("用户名或密码错误!"); } else if ("1".equals(user.getStatus())) { throw new UsernameNotFoundException("该用户已被封禁!"); } // 第三个是拥有权限 return new User(user.getUsername(), user.getPassword(), getUserAuthority()); } // 权限 private List<GrantedAuthority> getUserAuthority() { return new ArrayList<>(); } }
再来说下前端,先配置跨域
module.exports = { devServer: { host: '0.0.0.0', // 可以忽略不写 port: 8080, // 它是用来修改你打开后的端口号的 open: true, // 值为 true的话,项目启动时自动打开到浏览器里边, false不会打开 proxy: { '/api': { target: 'http://localhost:82', // 跨域请求的公共地址 ws: false, // 也可以忽略不写,不写不会影响跨域 changeOrigin: true, // 是否开启跨域,值为 true 就是开启, false 不开启 pathRewrite: { '^/api': ''// 注册全局路径, 但是在你请求的时候前面需要加上 /api } } } } }
配置axios,因为配置了跨域,这个时候可以直接写api了
import axios from 'axios' const request = axios.create({ baseURL: '/api' // 因为配置跨域了 }) // 下面这是拦截器,拦截请求,自动加token令牌 request.interceptors.request.use( config => { config.headers.token = `${localStorage.getItem('token')}` return config } ) export default request
说下如何请求前端登录接口POST请求
登录接口就是login,传入username和password就可以了
export const Login = function (data) { return request({ method: 'post', url: 'login', data: qs.stringify(data) }) }