springsecurity6前后端分离

一、数据库

  1、user表

 

image

   2、user_login表

image

   3、role表

image

   4、user_role表

image

   5、authority表

image

   6、role_authority表

image

   7、resource表

image

   8、resource_authority表

image

 二、springsecurity

  1、pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
</parent>
......
<dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.59</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.42</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.32</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
</dependencies>

  2、application.yml

server:
  port: 8080
  servlet:
    encoding:
      charset: UTF-8
      enabled: true
      force: true
  tomcat:
    uri-encoding: UTF-8
spring:
  application:
    name: VUE_SECURITY
  datasource:
    url: jdbc:mysql://localhost:3306/vue-security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: xxx
    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      connection-timeout: 30000
      maximum-pool-size: 10
      idle-timeout: 600000
      max-lifetime: 1800000
      minimum-idle: 5
      connection-test-query: SELECT 1
mybatis:
  configuration:
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #控制台打印sql信息
    map-underscore-to-camel-case: true
  mapper-locations: classpath:mapper/**/*Mapper.xml
  type-aliases-package: com.example.model

  3、mapper文件

    a、UserMapper.xml

    <select id="getCustomUserDetailsByUsername" parameterType="String" resultMap="customUserDetailsResult">
        select t.login_name, t.phone, t.password, t2.code as role_code, t4.code as authority_codes, t5.name
        from user_login t
        inner join user_role t1 on t.user_id = t1.user_id and t1.status = 1 and t1.seq = 1
        inner join role t2 on t1.role_id = t2.id and t2.status = 1
        inner join role_authority t3 on t2.id = t3.role_id and t3.status = 1
        inner join authority t4 on t3.authority_id = t4.id and t4.status = 1
        inner join user t5 on t.user_id = t5.id and t5.status =1
        <where>
            t.status = 1
            <if test="username != null">
                and (t.login_name = #{username}
                or t.phone = #{username})
            </if>
        </where>
    </select>

    <resultMap id="customUserDetailsResult" type="customUserDetails">
        <result column="password" property="password"/>
        <result column="name" property="name"/>
        <result column="login_name" property="email"/>
        <result column="phone" property="phone"/>
        <result column="role_code" property="role"/>
        <collection property="authorityCodes" ofType="string" javaType="list">
            <result column="authority_codes"/>
        </collection>
    </resultMap>

    b、AuthorityMapper.xml

    <select id="isAnonymousAllowed">
        select t.id from authority t
        inner join resource_authority t1 on t.id = t1.authority_id and t1.status = 1
        inner join resource t2 on t1.resource_id = t2.id and t2.status = 1
        <if test="urls != null">
            and
            <foreach collection="list" item="url" open="(" separator="or" close=")">
                (t2.url = #{url} and t2.method = #{method})
            </foreach>
        </if>
        where t.status = 1 and t.code = 'user_anonymous'
    </select>

    <select id="checkAuthority">
        select t.id from authority t
        inner join resource_authority t1 on t.id = t1.authority_id and t1.status = 1
        inner join resource t2 on t1.resource_id = t2.id and t2.status = 1
        <if test="urls != null">
            and
            <foreach collection="urls" item="url" open="(" separator="or" close=")">
                (t2.url = #{url} and t2.method = #{method})
            </foreach>
        </if>
        where t.status = 1
        <if test="codes != null">
            and t.code in
            <foreach collection="codes" item="code" open="(" separator="," close=")">
                #{code}
            </foreach>
        </if>
    </select>

    c、ResourceMapper.xml

    <select id="exists" resultType="Long">
        select t.id from resource t where t.status = 1
        <if test="url != null">
            and t.url = #{url}
        </if>
        <if test="method != null">
            and t.method = #{method}
        </if>
        <if test="url == null || method == null">
            and 1 = 2
        </if>
    </select>

  4、model包

    a、CustomUserDetails.java

import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@NoArgsConstructor
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

    private String username;
    private String password;
    private String name;
    private String email;
    private String phone;
    private String role;
    private Collection<? extends GrantedAuthority> authorities;
    private List<String> authorityCodes;

    public CustomUserDetails(String role, Collection<? extends GrantedAuthority> authorities) {
        this.role = role;
        this.authorities = authorities;
    }

    public CustomUserDetails(String username, String password, String name, String email, String phone, String role,
        Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.name = name;
        this.email = email;
        this.phone = phone;
        this.role = role;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getPhone() {
        return phone;
    }

    public String getRole() {
        return role;
    }

    public List<String> getAuthorityCodes() {
        return authorityCodes;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    public void setAuthorityCodes(List<String> authorityCodes) {
        this.authorityCodes = authorityCodes;
    }

}

    b、User.java

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Long id;
    private String name;
    private String email;
    private String phone;
    private String gender;
    private Date birthday;
    private Integer status;
    private Date createDate;
    private Date updateDate;

}

    c、UserLogin.java

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLogin {

    private Long id;
    private Long userId;
    private String LoginName;
    private String phone;
    private String password;
    private Integer status;
    private Date createDate;
    private Date updateDate;

}

    d、Authority.java

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Authority {

    private Long id;
    private String code;
    private String introduce;
    private Integer status;
    private Date createDate;
    private Date updateDate;

}

    e、Resource.java

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Resource {

    private Long id;
    private String url;
    private String method;
    private String introduce;
    private Integer status;
    private Date createDate;
    private Date updateDate;

}

  5、vo包

    a、UserVo.java

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVo {
private String name;
}

   6、mapper包

    a、UserMapper.java

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.model.user.CustomUserDetails;

/**
 * @author Administrator
 */
@Mapper
public interface UserMapper {

    List<CustomUserDetails> getCustomUserDetailsByUsername(String username);

}

    b、AuthorityMapper.java

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

/**
 * @author Administrator
 */
@Mapper
public interface AuthorityMapper {

    List<Long> isAnonymousAllowed(List<String> urls, String method);

    List<Long> checkAuthority(List<String> urls, String method, List<String> codes);

}

    c、ResourceMapper.java

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

/**
 * @author Administrator
 */
@Mapper
public interface ResourceMapper {

    List<Long> exists(String url, String method);

}

  7、service包

    a、CustomUserDetailsService.java

import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * @author Administrator
 */
public interface CustomUserDetailsService extends UserDetailsService {}

    b、CustomUserDetailsServiceImpl.java

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.mapper.user.UserMapper;
import com.example.model.user.CustomUserDetails;

/**
 * @author Administrator
 */
@Service("customUserDetailsService")
public class CustomUserDetailsServiceImpl implements CustomUserDetailsService {

    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 判断username是否为空
        if (StringUtils.isBlank(username)) {
            throw new NullPointerException("用户名为空");
        }
        // 自定义的CustomUserDetails所需字段 username password email phone role authorities
        List<CustomUserDetails> customUserDetailses = userMapper.getCustomUserDetailsByUsername(username);
        if (customUserDetailses.size() > 0) {
            CustomUserDetails customUserDetails = customUserDetailses.get(0);
            // 处理权限
            List<String> authorityCodes = customUserDetails.getAuthorityCodes();
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            authorityCodes.stream().forEach(auth -> grantedAuthorities.add(new SimpleGrantedAuthority(auth)));
            String uname = customUserDetails.getName();
            return new CustomUserDetails(username, customUserDetails.getPassword(), uname, customUserDetails.getEmail(),
                customUserDetails.getPhone(), customUserDetails.getRole(), grantedAuthorities);
        }
        return null;
    }

    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

}

    c、AuthorityService.java

import java.util.List;

/**
 * @author Administrator
 */
public interface AuthorityService {

    boolean isAnonymousAllowed(String url, String method);

    boolean checkAuthority(String url, String method, List<String> codes);

}

    d、AuthorityServiceImpl.java

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.mapper.permission.AuthorityMapper;
import com.example.util.CommonUtils;

/**
 * @author Administrator
 */
@Service("authorityService")
public class AuthorityServiceImpl implements AuthorityService {

    private AuthorityMapper authorityMapper;

    @Override
    public boolean isAnonymousAllowed(String url, String method) {
        if (StringUtils.isBlank(url) || StringUtils.isBlank(method)) {
            return false;
        }
        List<String> urls = CommonUtils.dealUrlUseWildcard(url, true);
        List<Long> ids = authorityMapper.isAnonymousAllowed(urls, method);
        return ids.size() > 0 ? true : false;
    }

    @Override
    public boolean checkAuthority(String url, String method, List<String> codes) {
        if (StringUtils.isBlank(url) || StringUtils.isBlank(method) || codes.size() == 0) {
            return false;
        }
        List<String> urls = CommonUtils.dealUrlUseWildcard(url, true);
        List<Long> ids = authorityMapper.checkAuthority(urls, method, codes);
        return ids.size() > 0 ? true : false;
    }

    @Autowired
    public void setAuthorityMapper(AuthorityMapper authorityMapper) {
        this.authorityMapper = authorityMapper;
    }

}

    e、ResourceService.java

/**
 * @author Administrator
 */
public interface ResourceService {

    boolean exists(String url, String method);

}

    f、ResourceServiceImpl.java

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.mapper.permission.ResourceMapper;

/**
 * @author Administrator
 */
@Service("resourceService")
public class ResourceServiceImpl implements ResourceService {

    private ResourceMapper resourceMapper;

    @Override
    public boolean exists(String url, String method) {
        List<Long> list = resourceMapper.exists(url, method);
        return list.size() == 1 ? true : false;
    }

    @Autowired
    public void setResourceMapper(ResourceMapper resourceMapper) {
        this.resourceMapper = resourceMapper;
    }

}

  8、util包

    a、CommonUtils.java

import java.util.ArrayList;
import java.util.List;

/**
 * @author Administrator
 */
public class CommonUtils {

    public static final String ANONYMOUS_USER = "anonymous";

    /**
     * 将url处理成带有**通配符的
     * 
     * @param url
     * @param own
     * @return
     */
    public static List<String> dealUrlUseWildcard(String url, boolean own) {
        List<String> list = new ArrayList<>();
        // 添加自己
        if (own) {
            list.add(url);
        }
        String slash = "/";
        String wildcard = "/*";
        if (!slash.equals(url) && !wildcard.equals(url)) {
            while (!slash.equals(url) && !wildcard.equals(url)) {
                url = urlRecursion(url);
                if (!wildcard.equals(url)) {
                    list.add(url);
                }
            }
        }
        return list;
    }

    private static String urlRecursion(String url) {
        String wildcard = "/*";
        String slash = "/";
        if (url.equals(wildcard)) {
            return null;
        }
        // 判断url是否结尾带*
        else if (url.contains(wildcard)) {
            // url去掉/*
            url = url.substring(0, url.length() - 2);
            url = url.substring(0, url.lastIndexOf(slash));
            url = url + wildcard;
        } else if (countStrInOther(url, slash) > 1) {
            url = url.substring(0, url.lastIndexOf(slash));
            url = url + wildcard;
        }
        return url;
    }

    private static int countStrInOther(String source, String countStr) {
        int count = 0;
        int index = source.indexOf(countStr);
        while (index != -1) {
            count++;
            index = source.indexOf(countStr, index + countStr.length());
        }
        return count;
    }

}

    b、EncryptionUtils.java

import java.security.MessageDigest;

import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;

/**
 * @author Administrator
 */
@Slf4j
public class EncryptionUtils {

    private static final String SALT_VALUE = "hash_algorithm";
    private static final String ENCRYPTION_SHA256 = "SHA-256";
    private static final String ENCRYPTION_MD5 = "MD5";
    private static final String DEFAULT_ID_PREFIX = "{";
    private static final String DEFAULT_ID_SUFFIX = "}";

    /**
     * SHA-2加密,安全性高于SHA-1,sha256和sha512都属于SHA-2
     * 
     * @param input
     * @return
     */
    public static String sha256(String input) {
        try {
            if (StringUtils.isBlank(input)) {
                log.info("**********输入不能为空**********");
                throw new NullPointerException("输入不能为空");
            }
            // 添加盐值
            input = input + SALT_VALUE;
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(input.getBytes());
            byte[] md = digest.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : md) {
                sb.append(String.format("%02x", b));
            }
            return DEFAULT_ID_PREFIX + "SHA-256" + DEFAULT_ID_SUFFIX + sb.toString();
        } catch (Exception e) {
            System.out.println("**********sha256加密报错**********");
            e.printStackTrace();
        }
        return null;
    }

    public static String md5(String input) {
        try {
            if (StringUtils.isBlank(input)) {
                log.info("**********输入不能为空**********");
                throw new NullPointerException("输入不能为空");
            }
            // 添加盐值
            input = input + SALT_VALUE;
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(input.getBytes());
            byte[] md = digest.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : md) {
                sb.append(String.format("%02x", b));
            }
            return DEFAULT_ID_PREFIX + "MD5" + DEFAULT_ID_SUFFIX + sb.toString();
        } catch (Exception e) {
            System.out.println("**********md5加密报错**********");
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 当前只有sha256和md5加密
     * 
     * @param input
     * @param type
     * @return
     */
    public static String encryption(String input, String type) {
        try {
            if (StringUtils.isBlank(input)) {
                log.info("**********输入不能为空**********");
                throw new NullPointerException("输入不能为空");
            }
            if (StringUtils.isBlank(type) || !ENCRYPTION_SHA256.equals(type) || !ENCRYPTION_MD5.equals(type)) {
                type = ENCRYPTION_SHA256;
            }
            // 添加盐值
            input = input + SALT_VALUE;
            MessageDigest digest = MessageDigest.getInstance(type);
            digest.update(input.getBytes());
            byte[] md = digest.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : md) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            System.out.println("**********" + type + "加密报错**********");
            e.printStackTrace();
        }
        return null;
    }

}

    c、JwtUtils.java

import java.time.Instant;
import java.util.Date;
import java.util.Map;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import com.alibaba.fastjson2.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.config.JwtAuthentication;

import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;

/**
 * @author Administrator
 */
@Slf4j
public class JwtUtils {

    public static final String secret = "project";

    public static final Integer EXP_TIME = 60 * 60 * 24 * 30;

    public static final String TOKEN_NAME = "sec_tk";

    public static final String USER_AGENT = "User-Agent";

    /**
     * 根据验证和时间生成token
     * 
     * @param authentication 验证对象
     * @param expTime 过期时间
     * @return
     */
    public static String genToken(Authentication authentication, Integer expTime, String userAgent) {
        if (expTime == null) {
            expTime = EXP_TIME;
        }
        // 数据精确到毫秒
        Date expDate = new Date(System.currentTimeMillis() + expTime * 1000L);
        String authenticationJson = JSON.toJSONString(authentication);
        return JWT.create()
            // 配置过期时间
            .withExpiresAt(expDate).withSubject(TOKEN_NAME)
            // 设置接收方信息,登录用户信息
            .withClaim(USER_AGENT, userAgent).withAudience(authenticationJson)
            // 签证信息
            .sign(Algorithm.HMAC256(secret));
    }

    /**
     * 生成JWT token
     * 
     * @param authentication
     * @return
     */
    public static String genToken(Authentication authentication, String userAgent) {
        return genToken(authentication, EXP_TIME, userAgent);
    }

    /**
     * 根据map创建token
     * 
     * @param map 需要创建token的信息,保存在withAudience中
     * @param expTime 过期时间
     * @return
     */
    public static String genToken(Map<String, String> map, Integer expTime) {
        if (expTime == null) {
            expTime = EXP_TIME;
        }
        JWTCreator.Builder builder = JWT.create();
        // 设置过期时间
        builder.withExpiresAt(new Date(System.currentTimeMillis() + expTime * 1000L));
        map.forEach((key, value) -> {
            if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
                builder.withClaim(key, value);
            }
        });
        return builder.sign(Algorithm.HMAC256(secret));
    }

    public static String genToken(Map<String, String> map) {
        return genToken(map, EXP_TIME);
    }

    public static void tokenVerify(String token, String userAgent) {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
        jwtVerifier.verify(token);
        Date expiresAt = JWT.decode(token).getExpiresAt();
        // 判断时间是否过期
        if (!expiresAt.after(new Date())) {
            throw new TokenExpiredException("token已过期", Instant.now());
        }
        if (userAgent == null || !userAgent.equals(JWT.decode(token).getClaim(USER_AGENT).asString())) {
            throw new InvalidClaimException("token非法传递");
        }
        String json = JWT.decode(token).getAudience().get(0);
        JwtAuthentication authentication = JSON.parseObject(json, JwtAuthentication.class);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

}

  9、filter包

    a、JwtAuthenticationTokenFilter.java

import java.io.IOException;

import org.springframework.web.filter.OncePerRequestFilter;

import com.alibaba.fastjson2.JSONObject;
import com.example.config.ResponseVo;
import com.example.util.JwtUtils;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (JwtUtils.TOKEN_NAME.equals(cookie.getName())) {
                    try {
                        JwtUtils.tokenVerify(cookie.getValue(), request.getHeader(JwtUtils.USER_AGENT));
                    } catch (Exception e) {
                        e.printStackTrace();
                        response.setContentType("application/json;charset=UTF-8");
                        response.getWriter().write(JSONObject
                            .toJSONString(new ResponseVo<>(HttpServletResponse.SC_OK, "非法token", e.getMessage())));
                        return;
                    }
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

    b、LoginFilter.java

import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.springframework.security.authentication.AuthenticationServiceException;
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 com.alibaba.fastjson2.JSON;

import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;

/**
 * @author Administrator
 */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final String method = "POST";

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException {
        if (!method.equals(request.getMethod())) {
            throw new AuthenticationServiceException("请求方法错误,请求方法应为POST,当前请求方法是:" + request.getMethod());
        }
        Map<String, String> user = obtainUser(request);
        UsernamePasswordAuthenticationToken authentication =
            UsernamePasswordAuthenticationToken.unauthenticated(user.get("username"), user.get("password"));

        // 此处可能报错,AuthenticationManager可能为空
        return this.getAuthenticationManager().authenticate(authentication);
    }

    private Map<String, String> obtainUser(HttpServletRequest request) throws IOException {
        Map<String, String> user = new HashMap<>(2);
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            BufferedReader reader = request.getReader();
            StringBuffer sb = new StringBuffer();
            String line = null;
            if ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            reader.close();
            Map<String, String> map = JSON.parseObject(sb.toString(), Map.class);
            username = map.get("username");
            password = map.get("password");
            if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
                throw new AuthenticationServiceException("用户或密码为空");
            }
        }
        user.put("username", username);
        user.put("password", password);
        return user;
    }

}

  10、handler包

    a、LoginSuccessHandler.java

import java.io.IOException;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson2.JSONObject;
import com.example.config.ResponseVo;
import com.example.model.user.CustomUserDetails;
import com.example.util.JwtUtils;
import com.example.vo.UserVo;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException {
        // 添加cookie
        Cookie cookie =
            new Cookie(JwtUtils.TOKEN_NAME, JwtUtils.genToken(authentication, request.getHeader(JwtUtils.USER_AGENT)));
        cookie.setMaxAge(JwtUtils.EXP_TIME);
        response.addCookie(cookie);
        // 获取用户对象
        CustomUserDetails customUserDetails = (CustomUserDetails)authentication.getPrincipal();
        response.setContentType("application/json;charset=UTF-8");
        ResponseVo<UserVo> responseVo =
            new ResponseVo<>(HttpServletResponse.SC_OK, new UserVo(customUserDetails.getName()), "登陆成功");
        response.getWriter().write(JSONObject.toJSONString(responseVo));
    }

}

    b、LoginFailureHandler.java

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson2.JSONObject;
import com.example.config.ResponseVo;
import com.example.vo.UserVo;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException exception) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ResponseVo<UserVo> responseVo = new ResponseVo<>(HttpServletResponse.SC_UNAUTHORIZED, new UserVo(), "用户名或密码错误");
        response.getWriter().write(JSONObject.toJSONString(responseVo));
    }

}

    c、CustomAccessDeniedHandler.java

import java.io.IOException;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson2.JSONObject;
import com.example.config.ResponseVo;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
        AccessDeniedException accessDeniedException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ResponseVo<String> responseVo = new ResponseVo<>(HttpServletResponse.SC_FORBIDDEN, "权限不足", "授权失败");
        response.getWriter().write(JSONObject.toJSONString(responseVo));
    }
}

  11、config包

    a、CorsConfig.java

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author Administrator
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            // 是否发送cookie
            .allowCredentials(true).allowedOriginPatterns("*")
            .allowedMethods(new String[] {"GET", "POST", "PUT", "DELETE", "OPTION"}).allowedHeaders("*")
            .exposedHeaders("*");
    }

}

    b、CustomAuthenticationEntryPoint.java

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson2.JSONObject;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        ResponseVo<String> responseVo = new ResponseVo<>(HttpServletResponse.SC_FORBIDDEN, "权限不足", "认证失败");
        response.getWriter().write(JSONObject.toJSONString(responseVo));
    }
}

    c、CustomAuthorizationManager.java

import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;

import com.example.service.permission.AuthorityService;
import com.example.service.permission.ResourceService;

import jakarta.servlet.http.HttpServletRequest;

/**
 * @author Administrator
 */
@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private ResourceService resourceService;
    private AuthorityService authorityService;

    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext request) {
        AuthorizationManager.super.verify(authentication, request);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext request) {
        return new AuthorizationDecision(authorize(authentication, request).isGranted());
    }

    @Override
    public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext request) {
        HttpServletRequest req = request.getRequest();
        String url = req.getRequestURI();
        String method = req.getMethod();
        // 判断是否存在对应的resource
        boolean exists = resourceService.exists(url, method);
        if (!exists) {
            return new AuthorizationDecision(false);
        }
        // 匿名用户检查
        if (authentication == null || authentication.get().getName() == null) {
            return new AuthorizationDecision(authorityService.isAnonymousAllowed(url, method));
        }

        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
        // UserDetailsService创建的user的codes
        List<String> list = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        // 其他用户检查
        return new AuthorizationDecision(authorityService.checkAuthority(url, method, list));
    }

    @Autowired
    public void setResourceService(ResourceService resourceService) {
        this.resourceService = resourceService;
    }

    @Autowired
    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }

}

    d、JwtAuthentication.java

import java.security.Principal;
import java.util.Collection;

import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.example.model.user.CustomUserDetails;

/**
 * @author Administrator
 */
public class JwtAuthentication implements Authentication {

    private Collection<SimpleGrantedAuthority> authorities;
    private Object details;
    private boolean authenticated;
    private CustomUserDetails principal;
    private Object credentials;

    @Override
    public Collection<SimpleGrantedAuthority> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(Collection<SimpleGrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Object getDetails() {
        return details;
    }

    public void setDetails(Object details) {
        this.details = details;
    }

    @Override
    public boolean isAuthenticated() {
        return authenticated;
    }

    @Override
    public void setAuthenticated(boolean authenticated) {
        this.authenticated = authenticated;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }

    public void setPrincipal(CustomUserDetails principal) {
        this.principal = principal;
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }

    public void setCredentials(Object credentials) {
        this.credentials = credentials;
    }

    @Override
    public String getName() {
        if (this.getPrincipal()instanceof UserDetails userDetails) {
            return userDetails.getUsername();
        }
        if (this.getPrincipal()instanceof AuthenticatedPrincipal authenticatedPrincipal) {
            return authenticatedPrincipal.getName();
        }
        if (this.getPrincipal()instanceof Principal principal) {
            return principal.getName();
        }
        return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
    }

}

    e、ResponseVo.java

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ResponseVo<T> implements Serializable {

    private int code;

    private T data;

    private String msg;

}

    f、SecurityConfig.java

import java.util.Arrays;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.CsrfConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.example.filter.JwtAuthenticationTokenFilter;
import com.example.filter.LoginFilter;
import com.example.handler.CustomAccessDeniedHandler;
import com.example.handler.LoginFailureHandler;
import com.example.handler.LoginSuccessHandler;
import com.example.util.EncryptionUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * @author Administrator
 */
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {

    private AuthenticationConfiguration authenticationConfiguration;
    private CustomAuthorizationManager customAuthorizationManager;
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    private LoginSuccessHandler loginSuccessHandler;
    private LoginFailureHandler loginFailureHandler;

    /**
     * cors: 跨域配置 csrf:禁用跨站请求伪造,使用jwt permitAll:公开的api无需认证 anyRequest().authenticated():其他所有请求都需要认证
     * 
     * @param http 配置 HTTP 安全策略的类
     * @return SecurityFilterChain 处理 HTTP 请求的核心组件
     * @throws Exception 异常
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.cors(cors -> cors.configurationSource(corsConfigurationSource())).csrf(CsrfConfigurer::disable)
            .authorizeHttpRequests(auth -> auth.anyRequest().access(customAuthorizationManager))
            .exceptionHandling(eh -> eh.authenticationEntryPoint(customAuthenticationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler))
            .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class).build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTION"));
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                if (rawPassword == null) {
                    throw new IllegalArgumentException("rawPassword cannot be null");
                }
                return EncryptionUtils.sha256(rawPassword.toString());
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                if (encodedPassword == null || encodedPassword.length() == 0) {
                    log.info("Empty encoded password");
                    return false;
                }
                return encode(rawPassword).equals(encodedPassword);
            }
        };
    }

    /**
     * 启动注入会调用
     * 
     * @return LoginFilter 登录过滤器
     * @throws Exception 抛出异常
     */
    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
        loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
        return loginFilter;
    }

    @Autowired
    public void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationConfiguration = authenticationConfiguration;
    }

    @Autowired
    public void setCustomAuthorizationManager(CustomAuthorizationManager customAuthorizationManager) {
        this.customAuthorizationManager = customAuthorizationManager;
    }

    @Autowired
    public void setCustomAuthenticationEntryPoint(CustomAuthenticationEntryPoint customAuthenticationEntryPoint) {
        this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
    }

    @Autowired
    public void setCustomAccessDeniedHandler(CustomAccessDeniedHandler customAccessDeniedHandler) {
        this.customAccessDeniedHandler = customAccessDeniedHandler;
    }

    @Autowired
    public void setLoginSuccessHandler(LoginSuccessHandler loginSuccessHandler) {
        this.loginSuccessHandler = loginSuccessHandler;
    }

    @Autowired
    public void setLoginFailureHandler(LoginFailureHandler loginFailureHandler) {
        this.loginFailureHandler = loginFailureHandler;
    }

}

   12、controller包

    a、IndexController.java

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

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson2.JSONObject;
import com.example.config.JwtAuthentication;
import com.example.config.ResponseVo;
import com.example.model.user.CustomUserDetails;
import com.example.vo.UserVo;

import jakarta.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
@RestController
public class IndexController {

    @GetMapping("/user/info")
    public ResponseVo<UserVo> userInfo() {
        ResponseVo<UserVo> responseVo = new ResponseVo<>();
        JwtAuthentication authentication = (JwtAuthentication)SecurityContextHolder.getContext().getAuthentication();
        CustomUserDetails customUserDetails = (CustomUserDetails)authentication.getPrincipal();
        UserVo userVo = new UserVo(customUserDetails.getName());
        responseVo.setCode(HttpServletResponse.SC_OK);
        responseVo.setData(userVo);
        responseVo.setMsg("获取数据成功");
        return responseVo;
    }

    @GetMapping("/admin/api")
    public String adminApi() {
        Map<String, String> map = new HashMap<>(1);
        map.put("msg", "ok");
        return JSONObject.toJSONString(map);
    }

    @GetMapping("/ordinary/api")
    public String ordinaryApi() {
        Map<String, String> map = new HashMap<>(1);
        map.put("msg", "ok");
        return JSONObject.toJSONString(map);
    }

    @GetMapping("/anonymous/api")
    public String anonymousApi() {
        Map<String, String> map = new HashMap<>(1);
        map.put("msg", "ok");
        return JSONObject.toJSONString(map);
    }

}

三、Vue

  1、main.ts

import {createApp} from 'vue'

import App from './App.vue'
import { createPinia } from 'pinia'
import router from '@/router/index'

const app = createApp(App)
app.use(router)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')

  2、router/index.ts

import { createRouter, createWebHistory } from "vue-router"

export default createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            component: () => import('@/components/Swiper.vue')
        },
        {
            name: 'home',
            path: '/home',
            component: () => import('@/components/Swiper.vue')
        },
        {
            name: 'table',
            path: '/table',
            component: () => import('@/views/Table.vue')
        },
        {
            name: 'login',
            path: '/login',
            component: () => import('@/views/Login.vue')
        },
        {
            name: 'register',
            path: '/register',
            component: () => import('@/views/Register.vue')
        },
        {
            name: 'adminApi',
            path: '/adminPath',
            component: () => import('@/views/admin/Api.vue')
        },
        {
            name: 'userQuery',
            path: '/userQuery',
            component: () => import('@/views/person/Query.vue')
        },
        {
            name: 'userTest',
            path: '/userTest',
            component: () => import('@/views/anonymity/Person.vue')
        }
    ]
})

  3、store/user.ts

import { defineStore } from "pinia"
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
    // 用户名
    let username = ref('')

    function updateUserName(name: string) {
        username.value = name
    }

    return {
        username,
        updateUserName
    }

})

  4、Top.vue

<template>
    <div class="top">
        <div v-if="username">
            <h2>{{ username }}</h2>
            <button @click="logout">退出</button>
        </div>
        <div v-else class="user">
            <RouterLink :key="1" :to="{name: 'login'}" class="login">登录</RouterLink>
            <RouterLink :key="2" :to="{name: 'register'}" class="register">注册</RouterLink>
        </div>
    </div>
</template>

<script lang="ts" setup name="Top">
    import { onMounted } from 'vue'
    import { RouterLink } from 'vue-router'
    import { useUserStore } from '@/store/user'
    import { storeToRefs } from 'pinia'
    import axios from 'axios'

    let userStore = useUserStore()
    let { username } = storeToRefs(userStore)

    onMounted(async() => {
        //加载用户信息
        const appiClient = axios.create({
            baseURL: 'http://localhost',
            withCredentials: true
        })
        let {data} = await appiClient.get(
            '/user/info'
        )

        if (data.code == 200) {
            userStore.updateUserName(data.data.name)
        } else {
            console.info(data)
        }
    })

    function logout() {
        userStore.updateUserName('')
    }

</script>

<style scoped>
    .top {
        display: flex;
        width: 99vw;
        height: 17vh;
        background-color: aqua;
        justify-content: center;
        align-items: center;
        border: 2px #000 solid;
    }
    .top .user {
        width: 99vw;
        height: 17vh;
        display: flex;
        justify-content: end;
        align-items: center;
    }
    .top .user .login {
        padding: 5px 20px;
        border-radius: 5px;
        background-color: greenyellow;
        margin-right: 24px;
        text-decoration: none;
        color: green;
    }
    .top .user .login:hover {
        background-color: green;
        color: greenyellow;
        font-weight: bolder;
    }
    .top .user .register {
        padding: 5px 20px;
        border-radius: 5px;
        background-color: green;
        margin-right: 24px;
        text-decoration: none;
        color: greenyellow;
    }
    .top .user .register:hover {
        background-color: greenyellow;
        color: green;
        font-weight: bolder;
    }
</style>

  5、Login.vue

<template>
    <div class="login">
        <div class="login-box">
            <h2>用户登录</h2>
            <p class="login-input">
                <span>用户名:</span>
                <input type="text" name="username" class="username" v-model="username"/>
            </p>
            <p class="login-input">
                <span>密码:</span>
                <input type="text" name="password" class="password" v-model="password"/>
            </p>
            <p class="login-button">
                <input type="button" @click="login" value="登录">
            </p>
            <p v-if="msg">{{ msg }}</p>
        </div>
    </div>
</template>

<script lang="ts" setup name="Login">
    import axios from 'axios'
    import { ref } from 'vue'
    import { useUserStore } from '@/store/user'

    let username = ref('')
    let password = ref('')
    let msg = ref('')
    let userStore = useUserStore()

    async function login() {
        const appiClient = axios.create({
            baseURL: 'http://localhost',
            withCredentials: true
        })
        let {data} = await appiClient.post(
            '/login',
            {username: username.value, password: password.value},
            {headers: {"Content-Type": "application/json"}}
        )
        console.info(data.data.name)
        if (data.code == 200) {
            userStore.updateUserName(data.data.name)
        } else {
            msg = data.msg
        }
    }
</script>

<style scoped lang="scss">
    .login {
        height: 80vh;
        display: flex;
        justify-content: center;
        align-items: center;
        align-content: center;
        background-color: skyblue;
        background-image: url('@/assets/img/login-bgi.png');
        opacity: 0.9;
        .login-box {
            border: 1px solid black;
            width: 400px;
            height: 230px;
            border-radius: 5%;
            background-color: skyblue;
            box-shadow: 5px 5px 5px #473fe4;
            h2 {
                text-align: center;
            }
            .login-input {
                text-align: end;
                margin-right: 20px;

                .username {
                    width: 240px;
                    height: 24px;
                    border-radius: 5px;
                    padding: 3px;
                    margin: 0 10px;
                    font-size: 20px;
                    margin-right: 20px;
                }
                .password {
                    width: 240px;
                    height: 24px;
                    border-radius: 5px;
                    padding: 3px;
                    margin: 0 10px;
                    font-size: 20px;
                    margin-right: 20px;
                }
            }
            .login-button {
                text-align: center;
                input[type='button'] {
                    padding: 5px 20px;
                    border-radius: 5px;
                    background-color: aquamarine;
                }
                input[type='button']:hover {
                    background-color: aqua;
                    font-weight: bolder;
                }
            }
        }
    }
</style>

 

posted @ 2025-10-25 22:39  此时不卷何时卷  阅读(16)  评论(0)    收藏  举报