1. 服务端
1.1 依赖
<?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.3.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.javaboy</groupId> <artifactId>vms</artifactId> <version>0.0.1-SNAPSHOT</version> <name>vms</name> <description>vms project for Spring Boot</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <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</artifactId> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>5.1.27</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--pageHelper--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.10</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.74</version> </dependency> </dependencies> <build> <plugins> <!-- maven插件 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
1.2 配置文件
server: port: 8090 servlet: context-path: /vms address: spring: profiles: active: dev application: name: vms servlet: multipart: maxFileSize: 100MB maxRequestSize: 100MB # 数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/vms?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&serverTimezone=UTC data-username: root data-password: root druid: # 初始化时建立物理连接的个数, initial-size: 5 # 最小连接池数量 min-idle: 5 # 最大连接池数量 max-active: 20 # 获取连接时最大等待时间,单位毫秒 max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒 time-between-eviction-runs-millis: 60000 # 配置一个连接在池中最小生存的时间,单位毫秒 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 test-while-idle: true # 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true test-on-borrow: false # 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true test-on-return: false # 是否缓存preparedStatement,也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。mysql5.5+建议开启 pool-prepared-statements: true # 当值大于0时poolPreparedStatements会自动修改为true max-pool-prepared-statement-per-connection-size: 20 # 通过别名的方式配置扩展插件: stat:监控统计,wall:防sql注入,log4j:日志 filters: stat,wall,slf4j # 合并多个DruidDataSource的监控数据 use-global-data-source-stat: true # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 mybatis: # 注意:一定要对应mapper映射xml文件的所在路径 mapper-locations: classpath:/mapper/*Mapper.xml # 注意:对应实体类的路径 type-aliases-package: com.javaboy.vms.entity configuration: map-underscore-to-camel-case: true # 日志配置 logging: level: com.javaboy.vms.mapper: DEBUG
1.3 实体类
package com.javaboy.vms.entity; import lombok.Getter; import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serializable; import java.util.Collection; /** * 用户信息(VUser)实体类 * * @author gaoyang * @since 2021-04-20 14:26:27 */ @Getter @Setter public class VUser implements Serializable, UserDetails { private static final long serialVersionUID = -60957006911784869L; /** * 主键 */ private Integer id; /** * 姓名 */ private String name; /** * 手机号码 */ private String phone; /** * 住宅电话 */ private String telephone; /** * 联系地址 */ private String address; /** * 是否启用 */ private Boolean enabled; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 头像 */ private String userface; /** * 备注 */ private String remark; /** * 为用户赋予角色 * * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } }
1.4 服务实现类
package com.javaboy.vms.service.impl; import com.javaboy.vms.mapper.VUserMapper; import com.javaboy.vms.entity.VUser; import com.javaboy.vms.service.VUserService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; /** * 用户信息(VUser)表服务实现类 * * @author gaoyang * @since 2021-04-20 14:26:28 */ @Service("vUserService") public class VUserServiceImpl implements VUserService, UserDetailsService { @Resource private VUserMapper vUserMapper; /** * 根据用户名加载用户对象 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { VUser user = vUserMapper.loadUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户名不存在!"); } return user; } }
1.5 xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.javaboy.vms.mapper.VUserMapper"> <resultMap type="com.javaboy.vms.entity.VUser" id="VUserMap"> <result property="id" column="id" jdbcType="INTEGER"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="phone" column="phone" jdbcType="VARCHAR"/> <result property="telephone" column="telephone" jdbcType="VARCHAR"/> <result property="address" column="address" jdbcType="VARCHAR"/> <result property="enabled" column="enabled" jdbcType="BOOLEAN"/> <result property="username" column="username" jdbcType="VARCHAR"/> <result property="password" column="password" jdbcType="VARCHAR"/> <result property="userface" column="userface" jdbcType="VARCHAR"/> <result property="remark" column="remark" jdbcType="VARCHAR"/> </resultMap> <!-- 根据用户名查询用户对象 --> <select id="loadUserByUsername" resultMap="VUserMap"> select id, name, phone, telephone, address, enabled, username, password, userface, remark from vms.v_user where username = #{username} </select> </mapper>
1.6 登录项配置
继续完善 SecurityConfig 配置类,重写 configure(HttpSecurity http) 方法:
package com.javaboy.vms.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.javaboy.vms.entity.VUser; import com.javaboy.vms.service.impl.VUserServiceImpl; import com.javaboy.vms.util.ResultDTO; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.*; 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.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.annotation.Resource; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @author: gaoyang * @date: 2021-04-15 16:35 * @description: Spring Security 配置类 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private VUserServiceImpl vUserService; @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(vUserService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .loginProcessingUrl("/doLogin") .usernameParameter("username") .passwordParameter("password") // 登录成功回调 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); VUser vUser = (VUser) authentication.getPrincipal(); vUser.setPassword(null); ResultDTO resultDTO = ResultDTO.success("登录成功", vUser); String s = new ObjectMapper().writeValueAsString(resultDTO); out.write(s); out.flush(); out.close(); } }) // 登录失败回调 .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); ResultDTO resultDTO = ResultDTO.error("登录失败"); if (exception instanceof LockedException) { resultDTO.setMsg("账户被锁定,请联系管理员!"); } else if (exception instanceof CredentialsExpiredException) { resultDTO.setMsg("密码过期,请联系管理员!"); } else if (exception instanceof AccountExpiredException) { resultDTO.setMsg("账户过期,请联系管理员!"); } else if (exception instanceof DisabledException) { resultDTO.setMsg("账户被禁用,请联系管理员!"); } else if (exception instanceof BadCredentialsException) { resultDTO.setMsg("用户名或者密码输入错误,请重新输入!"); } out.write(new ObjectMapper().writeValueAsString(resultDTO)); out.flush(); out.close(); } }) .permitAll() .and() .logout() // 登出回调 .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(ResultDTO.success("注销成功!"))); out.flush(); out.close(); } }) .permitAll() .and() .csrf().disable() // 没有认证时,在这里处理结果,不要重定向 .exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); ResultDTO resultDTO = ResultDTO.error("访问失败"); if (e instanceof InsufficientAuthenticationException) { resultDTO.setMsg("请求失败,请联系管理员!"); } out.write(new ObjectMapper().writeValueAsString(resultDTO)); out.flush(); out.close(); } }); } }
其中:
- loginPage 表示定义登录页。
定义了登录页面为 /login 的时候,Spring Security 也会帮我们自动注册一个 /login 的接口,这个接口是 POST 请求,用来处理登录逻辑。
- loginProcessingUrl 指定登录接口地址。
- usernameParameter、passwordParameter 登录参数配置。
登录表单中的参数默认是 username 和 password,如果需要改变登录参数的字段名只需在这里配置即可。
- and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
- permitAll 表示登录相关的页面/接口不要被拦截。
1.7 登录接口
这里登录不成功返回提示信息。
package com.javaboy.vms.controller; import com.javaboy.vms.util.ResultDTO; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author: gaoyang * @date: 2021-04-20 13:23 * @description: */ @RestController public class Login { /** * 未登录返回提示信息 * @return */ @RequestMapping("/login") public ResultDTO<Void> login(){ return ResultDTO.error("尚未登录,请登录"); } }
1.8 登录测试
1. 客户端
web 端代码就不贴了,源码: