不知道抽起哪根筋,花了一周的时间终于写了个基于springsecurity的登录的样例,4、5年前在学springboot时曾经按书上的例子却怎么也写不出来,算是了却自己的一桩心愿吧。当然,我不是程序员,java水平基本是百度水平,在网上找了一通所用的代码及技术均偏老旧,有些方法在新版本代码都不建议使用了,故自己折腾了好一翻才整出基于springboot+springsecurity+mybatis-plus+thymeleaf能正常运行的工程,在此详细介绍一下。
1.工程环境
java 1.8(想使用java 17的,但编译出错,原因就不细究了)
spring-boot-starter 2.7.6
thymeleaf-extras-springsecurity5 3.0.4
mybatis-plus-boot-starter 3.5.2
2.项目介绍
2.1.工程目录
我没有经过系统的学习,所以项目的目录建得凌乱,姑且不论,主要包括数据库部分(mybatis-plus)、权限部分(springsecurity)及前端3部分内容。
以下整个工程的目录:
以下是项目整个pom.xml的内容:
<?xml versinotallow="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.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.vuedemo</groupId>
<artifactId>vuedemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vuedemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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-thymeleaf</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
<version>0.0.20131108.vaadin1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以下是项目的属性yaml文件:
# DataSource Config
spring:
thymeleaf:
cache: true
enabled: true
datasource:
username: vuedemo
password: abcd1234
url: jdbc:mysql://localhost:3306/vuedemo?useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis-plus输出sql的日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.2.mybatis-plus实现数据操作
(1)数据库建表
根据sping的权限管理的标准,我准备了三张表,分别是用户表(sec_user)、角色表(sec_roles)、用户角色表(sec_roles_user),表的创建语句及样例数据如下所示,然后里面有一张视图(v_sec_user_roles)方便读取用户信息的,因为我不太会用mybatis-plus关联读取,故直接弄了个视图。
--用户表
CREATE TABLE `sec_user` (
`id` varchar(32) NOT NULL,
`user_name` varchar(50) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`create_time` date DEFAULT NULL,
`update_time` date DEFAULT NULL,
`delete_flag` int DEFAULT '0' COMMENT '0 - 有效;1 - 无效',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'
--角色表
CREATE TABLE `sec_roles` (
`id` varchar(10) NOT NULL,
`role_name` varchar(30) DEFAULT NULL,
`create_time` date DEFAULT NULL,
`update_time` date DEFAULT NULL,
`delete_flag` int DEFAULT '0' COMMENT '0 - 有效;1 - 无效',
PRIMARY KEY (`id`),
UNIQUE KEY `sec_roles_id_uindex` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色表'
--用户角色表
CREATE TABLE `sec_roles_user` (
`id` varchar(32) DEFAULT NULL,
`role_id` varchar(10) DEFAULT NULL,
`user_id` varchar(32) DEFAULT NULL,
`create_time` date DEFAULT NULL,
`update_time` date DEFAULT NULL,
`delete_flag` int DEFAULT '0' COMMENT '0 - 有效;1 - 无效'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色对照表'
--用户角色视图
CREATE VIEW `v_sec_user_roles` AS
(
SELECT
`ru`.`user_id` AS `user_id`,
`u`.`user_name` AS `user_name`,
`u`.`password` AS `password`,
`ru`.`role_id` AS `role_id`,
`r`.`role_name` AS `role_name`
FROM
((`sec_roles_user` `ru`
LEFT JOIN `sec_user` `u` ON ((`ru`.`user_id` = `u`.`id`)))
LEFT JOIN `sec_roles` `r` ON ((`ru`.`role_id` = `r`.`id`)))
WHERE
((`ru`.`delete_flag` = 0)
AND (`r`.`delete_flag` = 0)
AND (`u`.`delete_flag` = 0))
);
--插入用户数据
INSERT INTO vuedemo.sec_user (id,user_name,password,create_time,update_time,delete_flag) VALUES
('a31087d65d943a1880e322984bd6ef9b','Joey','abcd1234','2022-11-25','2022-11-25',0),
('af2e164dd8d9b7600521b8e561599aa4','Lily','1234abcd','2022-11-25','2022-11-25',0);
--插入角色数据
INSERT INTO vuedemo.sec_roles (id,role_name,create_time,update_time,delete_flag) VALUES
('1001','ADMIN','2022-11-25','2022-11-25',0),
('1002','USER','2022-11-25','2022-11-25',0);
--插入用户角色数据
INSERT INTO vuedemo.sec_roles_user (id,role_id,user_id,create_time,update_time,delete_flag) VALUES
('27421cb061870d9c02a70d4311f72a0f','1001','a31087d65d943a1880e322984bd6ef9b','2022-11-25','2022-11-25',0),
('5d6bafa94d263931b40d50b28f74ea8a','1002','af2e164dd8d9b7600521b8e561599aa4','2022-11-25','2022-11-25',0);
(2)添加依赖
在pom.xml文件中添加以下依赖,我这里用到了lombok,用于简化实体类的get()、set()方法,用起来还挺有意思的。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
(3)配置数据库
参考上面application.yaml文件的配置,对数据库连接及mybatis-plus进行配置,文件中log-impl参数是为了在后台打印mybatis生成的sql语句的,不需要可以不配置。
(4)mybatis-plus的配置类
为了实现mybatis用注释的方式(FieldFill.INSERT、FieldFill.INSERT_UPDATE)自动生成create_time及update_time,我实现MetaObjectHandler的类并重写了两个相关的方法。
--MyMetaObjectHandler
package com.vuedemo.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
//实现FieldFill.INSERT、FieldFill.INSERT_UPDATE的注解
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
//插入时的填充策略
@Override
public void insertFill(MetaObject metaObject){
//System.out.println("********* createTime **********");
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
//System.out.println("***** updateTime ********");
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
为了让mybatis实现分页,对MybatisPlusInterceptor进行了注册,但本项目实际没有用到,我也只是在单元测试里玩了一下而已。
--MybatisConfig
package com.vuedemo.config.db;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
/**
* 注册插件
* @return
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor(){
//System.out.println("********* 注册分页插件 **********");
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置请求的页面大于最大页后,true返回首页,false继续请求。默认false
paginationInnerInterceptor.setOverflow(false);
// 单页记录条数的限制,默认无限制
paginationInnerInterceptor.setMaxLimit(500L);
// 设置数据库的类型
paginationInnerInterceptor.setDbType(DbType.MYSQL);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
(5)实体类及mapper的实现
通过mybatis-plus及lombok,写实体类及mapper是相当简单的,而且通过IdType.ASSIGN_UUID的注释让表自动生成ID,减轻DBA的运维难度。具体代码如下:
--SecRoles
package com.vuedemo.entity.sec;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class SecRoles implements Serializable {
private String id;
private String roleName;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
private int deleteFlag;
}
--SecRolesUser
package com.vuedemo.entity.sec;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class SecRolesUser implements Serializable {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String roleId;
private String userId;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
private int deleteFlag;
}
--SecUser
package com.vuedemo.entity.sec;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
/**
* 用户表(SecUser)表实体类
*
* @author makejava
* @since 2022-11-25 11:57:34
*/
@SuppressWarnings("serial")
@Data
public class SecUser implements Serializable {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String userName;
private String password;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
private Integer deleteFlag;
}
--VSecUserRoles,这是查用户信息的视图
package com.vuedemo.entity.sec;
import lombok.Data;
import java.io.Serializable;
@Data
public class VSecUserRoles implements Serializable {
private String userId;
private String userName;
private String password;
private String roleId;
private String roleName;
}
--SecRolesMapper,使用mybatis-plus实现mapper一句代码而已,相当方便
package com.vuedemo.mapper.sec;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vuedemo.entity.sec.SecRoles;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SecRolesMapper extends BaseMapper<SecRoles> {
}
--SecRolesUserMapper
package com.vuedemo.mapper.sec;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vuedemo.entity.sec.SecRolesUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SecRolesUserMapper extends BaseMapper<SecRolesUser> {
}
--SecUserMapper
package com.vuedemo.mapper.sec;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vuedemo.entity.sec.SecUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SecUserMapper extends BaseMapper<SecUser> {
}
--VSecUserRolesMapper
package com.vuedemo.mapper.sec;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vuedemo.entity.sec.VSecUserRoles;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VSecUserRolesMapper extends BaseMapper<VSecUserRoles> {
}
(6)用户信息的服务类实现
再通过提供一个服务类去获取数据库里的用户信息,代码如下:
--SecUserService
package com.vuedemo.services;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.vuedemo.entity.sec.SecUser;
import com.vuedemo.entity.sec.VSecUserRoles;
import com.vuedemo.mapper.sec.SecUserMapper;
import com.vuedemo.mapper.sec.VSecUserRolesMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class SecUserService {
@Autowired(required = false)
SecUserMapper userMapper;
@Autowired(required = false)
VSecUserRolesMapper userRolesMapper;
/**
* 根据用户名获取用户信息
* @param username
* @return
*/
public SecUser getUserByUsername(String username){
System.out.println("************ getUSerByUsername************");
QueryWrapper<SecUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", username);
queryWrapper.eq("delete_flag", 0);
return userMapper.selectOne(queryWrapper);
}
/**
* 根据用户ID获取用户角色
* @param user_id
* @return
*/
public List<VSecUserRoles> getRolesByUSerId(String user_id){
QueryWrapper<VSecUserRoles> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", user_id);
List<VSecUserRoles> roles = userRolesMapper.selectList(queryWrapper);
return roles;
}
}
(6)测试
这时侯你事实上是可以通过测试类试一下上述的service是否能正常运行,在这里我其实折腾了挺久的,在此附上测试类的代码:
import com.vuedemo.VueDemoApplication;
import com.vuedemo.mapper.sec.SecRolesMapper;
import com.vuedemo.mapper.sec.SecRolesUserMapper;
import com.vuedemo.mapper.sec.SecUserMapper;
import com.vuedemo.services.SecUserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
//必须要带上app类的参数classes = VueDemoApplication.class
@SpringBootTest(classes = VueDemoApplication.class)
class Tests {
@Resource
private SecUserService userService;
@Test
public void getUserInfo(){
System.out.println(userService.getUserByUsername("Joey".toUpperCase()));
}
}
对了,很关键的是Application主类要加上mapper的扫描注释。
package com.vuedemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
//@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@SpringBootApplication
@MapperScan("com.vuedemo.mapper") //扫描mapper
public class VueDemoApplication {
public static void main(String[] args){
SpringApplication.run(VueDemoApplication.class, args);
}
}
2.2.SpringSecurity的实现
实现权限的控制,大体的思路是:
1.确定登录的前端入口:/login
2.登录时用户信息的获取及权限的获取:实现UserDetailsService
3.登录失败的处理:实现SimpleUrlAuthenticationFailureHandler
4.访问错误的处理:/400 /403 /500
5.注销的处理:/logout
(1)权限控制的实现
以下WebSecurityConfig的配置类实现了权限的控制,在一些文章中使用了WebSecurityConfigurerAdapter,但这抽象类现在是不建议使用的,在IDEA中会有warning看着比较碍眼,这里用SecurityFilterChain来实现,也比较简单。
package com.vuedemo.config.sec;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
//认证失败结果处理器
@Resource
private AuthenticationFailureHandler failureHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.headers().frameOptions().disable();//开启运行iframe嵌套页面
httpSecurity
.csrf().disable()
.authorizeRequests()
//.antMatchers("/index").permitAll()
//.antMatchers("/users").hasRole("ADMIN")
.antMatchers("/500").permitAll()
.antMatchers("/403").permitAll()
.antMatchers("/404").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/default/js/**").permitAll()
.antMatchers("/default/css/**").permitAll()
.antMatchers("/default/svg/**").permitAll()
.antMatchers("/").permitAll()
.anyRequest() //其他请求
.authenticated() //均需要身份验证
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/dologin")
.defaultSuccessUrl("/login")
.failureHandler(failureHandler)//登录失败的处理方法
.and()
.logout()
//.logoutUrl("/login")//使用注销方法返回login的页面
.permitAll()
.and()
.sessionManagement()
.invalidSessionUrl("/login");
return httpSecurity.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
//使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
}
这里需要重点关注两点:
a.以下这个/dologin是前端post过来的action,但不需要在代码中实现的,springsecurity会自动实现这个方法的,只要确保post的方法名正确即可。
.loginProcessingUrl("/dologin")
b.静态资源一般会放在static文件夹上,但也是需要授权的,在授权时需要注意路径是没有“/static”的。
.antMatchers("/default/js/**").permitAll()
.antMatchers("/default/css/**").permitAll()
.antMatchers("/default/svg/**").permitAll()
(2)UserDetailsService的实现
以下服务类实现从DB中获取用户信息(通过SecUserService),这里的密码是没有加密写到表的,后续要注意密码的加密。
package com.vuedemo.services;
import com.vuedemo.entity.sec.SecUser;
import com.vuedemo.entity.sec.VSecUserRoles;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Resource
private SecUserService userService;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SecUser user = userService.getUserByUsername(username.toUpperCase());
if(user == null){
throw new UsernameNotFoundException("not found");
}
//定义权限列表
List<VSecUserRoles> roles = userService.getRolesByUSerId(user.getId());
List<GrantedAuthority> authorities = new ArrayList<>();
roles.forEach(r -> {
authorities.add(new SimpleGrantedAuthority("ROLE_" + r.getRoleName()));
});
System.out.println("************ ROLE **************");
authorities.forEach(System.out::println);
User userDetails = new User(user.getUserName(), passwordEncoder.encode(user.getPassword()), authorities);
return userDetails;
}
}
(3)FailureHandler的实现
当无法获取用户信息时,需要对此进行处理,故要实现一个SimpleUrlAuthenticationFailureHandler的抽象类,代码如下:
package com.vuedemo.services;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//super.onAuthenticationFailure(request, response, exception); //如果不注释此行会redirect失败
//System.out.println("*************** 登录失败 *************");
this.saveException(request, exception);
this.getRedirectStrategy().sendRedirect(request, response, "/login?error=true");
}
}
理论上成功也可以做处理的,只是我这里懒得实现SuccessHandler。
(4)访问请求的实现
通过一个controller实现页面的访问(包括错误提示页面),结合thymeleaf的模板返回对应的页面,注意以下/logout请求的实现即可。
package com.vuedemo.controller.view;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
public class MainViewController {
@RequestMapping(path = {"/index","/"})
public String index(){
return "index";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if(auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "login";
}
//找不到页面
@RequestMapping("/404")
public String notFoundPage(){
return "/error/404";
}
//未授权
@RequestMapping("/403")
public String accessError(){
return "/error/403";
}
//服务器错误
@RequestMapping("/500")
public String internalError(){
return "/error/500";
}
}
2.3.前端的实现
(1)资源目录
我这里使用了bootstraps框架,发现那些css的用法基本忘光了,statich目录的资源需要在springsecurity进行授权(前面有提到过)。
(2)login页面的实现
我设计login及logout成功都返回同一个画面,结合sec:标签可以这么写:
--login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>login</title>
<link th:href="@{default/css/bootstrap.css}" type="text/css" rel="stylesheet">
<link th:href="@{default/css/signin.css}" type="text/css" rel="stylesheet">
</head>
<body class="text-center">
<main class="form-signin" sec:authorize="isAuthenticated()">
<form th:action="@{/logout}" method = "get">
<img class="mb-4" th:src="@{default/svg/hi.svg}" alt width="80" height="80">
<h1 class="h3 mb-3 fw-normal">Welcome , <span sec:authentication="name"></span></h1>
<div></div>
<button class="w-100 btn bth-lg btn-danger" type="submit">Sign out</button>
</form>
</main>
<main class="form-signin" sec:authorize="isAnonymous()">
<form th:action="@{/dologin}" method = "post">
<img class="mb-4" th:src="@{default/svg/hi.svg}" alt width="80" height="80">
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input type="text" name="username" class="form-control" id="floatingInput">
<label for="floatingInput">Username</label>
</div>
<div class="form-floating">
<input type="password" name="password" class="form-control" id="floatingPassword">
<label for="floatingPassword">Password</label>
</div>
<div></div>
<button class="w-100 btn bth-lg btn-primary" type="submit">Sign in</button>
<div></div>
<span class="alert alert-warning" style="margin-top: 10px" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
</form>
</main>
</body>
</html>
一定要注意login的action一定要与权限配置中的loginProcessingUrl保持一致,否则无法调通的。
这里我从bootstrap的官网上找了个signin.css的文件,也将源码给出来吧:
--signin.css
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
至于其他error或index的页面,我基本长这样:
--index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>I AM INDEX !</h1>
</body>
</html>
(3)效果
基本上代码就这么,看一下效果。
访问/login时,没有权限,是长这样的:
输入账号密码后,页面长这样:
当你访问其他有权限的页面是可以正常访问,如:
3.总结
a.mybatis-plus的实现相对简单一点,网上资料很多;
b.springsecurity的实现要注意HttpSecurity的权限控制,理解antMatchers等方法的意义;
c.thymeleaf+springsecurity的用法,一定要注意版本适配的问题,可以参考我的另一篇blog(https://blog.51cto.com/joeyliu/5907101),为了这个我花了足足一天的功夫。
源码下载地址:https://ost.51cto.com/resource/2429
希望对大家有所帮助!!谢谢!!