基于springboot+thymeleaf+springsecurity搭建一套web小案例

时间:2024-10-20 19:33:43

一、前言

 本案例中的源代码已上传到资源库,可自行下载,传送阵 https://download.****.net/download/qq_36260963/89906196

    Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的,使用它可以做到专注于Spring应用的开发,而无需过多关注XML的配置。学习框架就是学习配置

​ 简单来说,它提供了一堆依赖打包Starter,并已经按照使用习惯解决了依赖问题—习惯大于约定。Spring Boot默认使用tomcat作为服务器,使用logback提供日志记录。无需多言,直接进入节奏.

   Thymeleaf是一个流行的现代服务器端Java模板引擎,它专门设计用于Web和独立环境中的应用程序。它允许开发者以清晰和直观的方式将服务器端的数据与HTML、XML、JavaScript、CSS以及纯文本等模板文件结合起来。Thymeleaf的最大特点之一是它的“自然模板”技术,这意味着开发者可以编写标准的HTML代码,并通过Thymeleaf特有的属性和表达式(如th:text、th:if等)来动态插入或修改内容,而无需改变HTML的结构或引入特定的模板语法。

  Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

二、效果演示

密码错误

登录成功

三、后端代码

四、springsecurity 相关

主要是springsecurity config 配置相关,包含开启表单登录,url地址拦截与放行,退出登录;以及自定义userdetailservice 接口,实现自己的登录逻辑

 4.1 springsecurity  核心配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/user/**","/","/login","/register").permitAll() //允许访问
                .anyRequest().authenticated() // 其他都需要认证
                .and().formLogin() // 开启表单登录
                .loginProcessingUrl("/doLogin")
                .usernameParameter("username")
                .passwordParameter("passwd")
                .loginPage("/login")
                .successForwardUrl("/employee/lists")// 成功后跳转的url
                .failureUrl("/login")
                .and().logout()
                .logoutSuccessUrl("/login")//退出登录后跳转的页面
                .and().csrf().disable(); // 关闭csrf 防护
    }

}

4.2 用户登录相关

@Service
public class UserServiceImpl implements UserService, UserDetailsService {

    @Autowired(required = false)
    private UserMapper userMapper;

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public User login(String username, String password) throws IllegalAccessException {
        User queryUsr = userMapper.findByUserName(username);
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            throw new IllegalAccessException("用户名和密码不能为空");
        }

        if (ObjectUtils.isEmpty(queryUsr)) {
            throw new IllegalAccessException("用户不存在");
        }


        String encPwd = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
        logger.info("原密码:{},加密后的密码为:{}",password,encPwd);
        // 密码对比DigestUtils
        if (!encPwd.equals(queryUsr.getPassword())) {
            throw new IllegalAccessException("密码错误!");
        }
        return queryUsr;
    }

    @Override
    public void addUser(User user) throws IllegalAccessException{
        if (!StringUtils.hasLength(user.getUsername()) || !StringUtils.hasLength(user.getPassword())) {
            throw new IllegalAccessException("用户名和密码不能为空");
        }
        //查看用户名是否重复
        User queryUser = userMapper.findByUserName(user.getUsername());
        if (!ObjectUtils.isEmpty(queryUser)) {
            throw new IllegalAccessException("用户【"+user.getUsername()+"】已存在,请更换用户名!");
        }

        String password = user.getPassword();
        String encPwd = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
        user.setPassword(encPwd);

        //添加
        userMapper.save(user);
        logger.info("添加用户成功!");

    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!StringUtils.hasLength(username) ) {
            throw new UsernameNotFoundException("用户名不能为空");
        }

        User queryUsr = userMapper.findByUserName(username);

        if (ObjectUtils.isEmpty(queryUsr)) {
            throw new UsernameNotFoundException("用户不存在");
        }


        LoginSessionUser sessionUser = new LoginSessionUser(queryUsr);

        return sessionUser;
    }
}

4.3 userdetail 实体类

public class LoginSessionUser implements UserDetails {

    private User user;

    public LoginSessionUser(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_amin"));
    }

    @Override
    public String getPassword() {
        return "{MD5}"+user.getPassword();
    }

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

五、spring mvc 配置

用来配置静态资源拦截,常规url 以及viername配置

5.1 mvc 核心配置

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Value("${photo.file.dir}")
    private String dir;

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //viewController 请求路径    viewName: 跳转视图
        registry.addViewController("/").setViewName("redirect:/login");
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/register").setViewName("regist");
        registry.addViewController("/addEmp").setViewName("addEmp");
    }


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");

        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/**")
                .addResourceLocations("file:"+dir);
    }

}

六、yml 配置相关

1、数据库相关配置

2、日志配置

3、静态文件配置

4、模版解析器 thymeleaf 配置

server:
  port: 8082

spring:
  ## 数据库配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/springboot_ems_db?characterEncoding=UTF-8

  web:
    #静态文件配置
    resources:
      static-locations: classpath:/static/,file:${photo.file.dir}
  mvc:
    static-path-pattern: /static/**


  # thymeleaf 模版配置
  thymeleaf:
    cache: false
    suffix: .html
    prefix: classpath:/templates/
    mode: html

# mybatis 配置
mybatis:
  mapper-locations: classpath:/mapper/mysql/*.xml
  type-aliases-package: com.fashion.entity


# 日志配置
logging:
  level:
    com.fashion: debug

photo:
  file:
    dir: j:\java\project\springboot-study\springboot-ems-security\images\

七、前端相关页面

这里只是展示部分thymeleaf 的部分,因为过多页面,需要可自行下载zip文件包

7.1 登录页面

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en" xmlns:th="http://www.thymeleaf.org">
	<head>
		<title>login</title>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}" />
	</head>

	<body>
		<div id="wrap">
			<div id="top_content">
					<div id="header">
						<div id="rightheader">
							<p>
								<span th:text="${#dates.format(#dates.createNow(), 'yyyy-MM-dd HH:mm:ss')}"/>
								<br />
							</p>
						</div>
						<div id="topheader">
							<h1 id="title">
								<a th:href="@{/login }">main</a>
							</h1>
						</div>
						<div id="navigation">
						</div>
					</div>
				<div id="content">
					<p id="whereami">
					</p>
					<h1>
						欢迎进入,请登录!
						<!--<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION }" style="color: red;"/>
						<span th:text="${session.errMsg }" style="color: deeppink;"/>-->
						<span th:if="${session.SPRING_SECURITY_LAST_EXCEPTION != null}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message }" style="color: deeppink;"/>
					</h1>
					<form th:action="@{/doLogin }" method="post">
						<table cellpadding="0" cellspacing="0" border="0"
							class="form_table">
							<tr>
								<td valign="middle" align="right">
									用户名:
								</td>
								<td valign="middle" align="left">
									<input type="text" class="inputgri" name="username" />
								</td>
							</tr>
							<tr>
								<td valign="middle" align="right">
									密码:
								</td>
								<td valign="middle" align="left">
									<input type="password" class="inputgri" name="passwd" />
								</td>
							</tr>
						</table>
						<p>
							<input type="submit" class="button" value="点我登录 &raquo;" />
							&nbsp;&nbsp;
							<a th:href="@{/register}">还没有账号,立即注册</a>
						</p>
					</form>
				</div>
			</div>
			<div id="footer">
				<div id="footer_bg">
					ABC@126.com
				</div>
			</div>
		</div>
	</body>
</html>

7.2 登录成页面

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extrasspringsecurity5">
	<head>
		<title>emplist</title>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}" />
	</head>
	<body>
		<div id="wrap">
			<div id="top_content"> 
				<div id="header">
					<div id="rightheader">
						<p>
							<span th:text="${#dates.format(#dates.createNow(), 'yyyy-MM-dd HH:mm:ss')}"/>
							<br />
							<span sec:authorize="isAuthenticated()">
								<a th:href="@{/logout }">安全退出</a>
							</span>

						</p>
					</div>
					<div id="topheader">
						<h1 id="title">
							<a th:href="@{/employee/lists }">main</a>
						</h1>
					</div>
					<div id="navigation">
					</div>
				</div>
				<div id="content">
					<p id="whereami">
					</p>
					<h1>
						欢迎
						<span sec:authorize="isAuthenticated()">
							<span sec:authentication="principal.username"></span>
						</span>
					</h1>
					<table class="table">
						<tr class="table_header">
							<td>
								编号
							</td>
							<td>
								姓名
							</td>
							<td>
								头像
							</td>
							<td>
								工资
							</td>
							<td>
								生日
							</td>
							<td>
								操作
							</td>
						</tr>
						<tr  th:each="emp,status:${employeeList }" th:class="${status.odd ? 'row1' : 'row2'}">
							<td>
								<span th:text="${emp.id }"/>
							</td>
							<td>
								<span th:text="${emp.name }"/>
							</td>
							<td>
								<img th:src="@{/ }+${emp.photo}" width="60">
							</td>
							<td>
								<span th:text="${emp.salary }"/>
							</td>
							<td>
								<span th:text="${#dates.format(emp.birthday,'yyyy年MM月hh日')}"/>
							</td>
							<td>
								<a  href="javascript:;" th:onclick="'delFn('+${emp.id}+');'">删除</a>&nbsp;<a href="javascript:;" th:onclick="'updFn('+${emp.id }+');'">更新</a>
							</td>
						</tr>

						</tr>
					</table>
					<script type="text/javascript">
						function delFn(id) {
							if (confirm("你真的要删除员工id为:"+id+"的记录吗?")) {
								location.href = '[[@{/employee/delEmployee?id= }]]'+id;
							}
						}

						function updFn(id) {
							location.href = '[[@{/employee/getDetail?id= }]]'+id;
						}
					</script>
					<p>
						<!--<input type="button" class="button" value="添加" onclick="location='addEmp.html'"/>-->
						<input type="button" class="button" value="添加" onclick="addEmp()"/>
						<script type="text/javascript">
							function addEmp() {
								location.href = '[[@{/addEmp}]]'
							}
						</script>
					</p>
				</div>
			</div>
			<div id="footer">
				<div id="footer_bg">
				ABC@126.com
				</div>
			</div>
		</div>
	</body>
</html>