原文地址:https://liuyanzhao.com/7431.html
本文通过一个登录的例子介绍 SpringBoot + Spring Security + Thymeleaf 权限管理。
一、数据库
用户登录账号是 admin,saysky,lockeduser
密码都是 123456
1、表结构
user 表
authority 表
user_authority 表
2、数据
user 表
authority 表
user_authority 表
3、SQL 代码
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for `authority`
-- ----------------------------
DROP TABLE IF EXISTS `authority`;
CREATE TABLE `authority` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of `authority`
-- ----------------------------
BEGIN;
INSERT INTO `authority` VALUES ('1', 'ROLE_ADMIN'), ('2', 'ROLE_USER');
COMMIT;
-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` varchar(100) NOT NULL,
`name` varchar(20) NOT NULL,
`email` varchar(50) NOT NULL,
`avatar` varchar(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`last_login_time` datetime DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_ob8kqyqqgmefl0aco34akdtpe` (`email`),
UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of `user`
-- ----------------------------
BEGIN;
INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '管理员', '[email protected]', null, null, null, 'normal'), ('2', 'saysky', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '言曌', '[email protected]', null, null, null, 'normal'), ('3', 'lockuser', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '锁定账号', '[email protected]', null, null, null, 'locked');
COMMIT;
-- ----------------------------
-- Table structure for `user_authority`
-- ----------------------------
DROP TABLE IF EXISTS `user_authority`;
CREATE TABLE `user_authority` (
`user_id` bigint(20) NOT NULL,
`authority_id` bigint(20) NOT NULL,
KEY `FKgvxjs381k6f48d5d2yi11uh89` (`authority_id`),
KEY `FKpqlsjpkybgos9w2svcri7j8xy` (`user_id`),
CONSTRAINT `FKgvxjs381k6f48d5d2yi11uh89` FOREIGN KEY (`authority_id`) REFERENCES `authority` (`id`),
CONSTRAINT `FKpqlsjpkybgos9w2svcri7j8xy` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of `user_authority`
-- ----------------------------
BEGIN;
INSERT INTO `user_authority` VALUES ('1', '1'), ('2', '2'), ('1', '2');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
二、Maven 依赖
pom.xml
- <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd”>
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.liuyanzhao</groupId>
- <artifactId>chuyun</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>war</packaging>
- <name>chuyun</name>
- <description>Chuyun Blog for Spring Boot</description>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>1.5.9.RELEASE</version>
- <relativePath/> <!– lookup parent from repository –>
- </parent>
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>1.8</java.version>
- <thymeleaf.version>3.0.3.RELEASE</thymeleaf.version>
- <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version>
- <elasticsearch.version>2.4.4</elasticsearch.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!–lombok–>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- <!– Thymeleaf –>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <!– Spring Data JPA–>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <!– mysql–>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!– 热部署 –>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-devtools</artifactId>
- <optional>true</optional>
- </dependency>
- <!– Spring Boot Elasticsearch 依赖 –>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
- </dependency>
- <dependency>
- <groupId>net.java.dev.jna</groupId>
- <artifactId>jna</artifactId>
- <version>4.3.0</version>
- </dependency>
- <!– Spring Security 依赖 –>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
- <dependency>
- <groupId>org.thymeleaf.extras</groupId>
- <artifactId>thymeleaf-extras-springsecurity4</artifactId>
- <version>3.0.2.RELEASE</version>
- </dependency>
- <!–fasthson–>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.7</version>
- </dependency>
- <!–验证码kaptcha–>
- <dependency>
- <groupId>com.github.penggle</groupId>
- <artifactId>kaptcha</artifactId>
- <version>2.3.2</version>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <configuration>
- <fork>true</fork>
- </configuration>
- </plugin>
- </plugins>
- </build>
- </project>
主要需要 SpringBoot、Thymeleaf、Spring Security 的依赖
三、实体类
User.java
- package com.liuyanzhao.chuyun.entity;
- import lombok.Data;
- import org.hibernate.validator.constraints.Email;
- import org.hibernate.validator.constraints.NotEmpty;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.authority.SimpleGrantedAuthority;
- import org.springframework.security.core.userdetails.UserDetails;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import javax.persistence.;
- import javax.validation.constraints.Size;
- import java.io.Serializable;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Date;
- import java.util.List;
- /
- @author 言曌
- @date 2017/12/28 上午9:06
- /
- @Entity // 实体
- @Data
- public class User implements UserDetails, Serializable {
- private static final long serialVersionUID = 1L;
- @Id // 主键
- @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
- private Long id; // 用户的唯一标识
- @NotEmpty(message = “昵称不能为空”)
- @Size(min=2, max=20)
- @Column(nullable = false, length = 20) // 映射为字段,值不能为空
- private String name;
- @NotEmpty(message = “邮箱不能为空”)
- @Size(max=50)
- @Email(message= “邮箱格式不对” )
- @Column(nullable = false, length = 50, unique = true)
- private String email;
- @NotEmpty(message = “账号不能为空”)
- @Size(min=3, max=20)
- @Column(nullable = false, length = 20, unique = true)
- private String username; // 用户账号,用户登录时的唯一标识
- @NotEmpty(message = “密码不能为空”)
- @Size(max=100)
- @Column(length = 100)
- private String password; // 登录时密码
- @Column(length = 200)
- private String avatar; // 头像图片地址
- private Date createTime;
- private Date lastLoginTime;
- @Column(length = 10)
- private String status;
- @ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
- @JoinTable(name = “user_authority”, joinColumns = @JoinColumn(name = “user_id”, referencedColumnName = “id”),
- inverseJoinColumns = @JoinColumn(name = “authority_id”, referencedColumnName = “id”))
- private List<Authority> authorities;
- protected User() { // JPA 的规范要求无参构造函数;设为 protected 防止直接使用
- }
- public User(String name, String email,String username,String password) {
- this.name = name;
- this.email = email;
- this.username = username;
- this.password = password;
- }
- public Collection<? extends GrantedAuthority> getAuthorities() {
- // 需将 List<Authority> 转成 List<SimpleGrantedAuthority>,否则前端拿不到角色列表名称
- List<SimpleGrantedAuthority> simpleAuthorities = new ArrayList<>();
- for(GrantedAuthority authority : this.authorities){
- simpleAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
- }
- return simpleAuthorities;
- }
- public void setAuthorities(List<Authority> authorities) {
- this.authorities = authorities;
- }
- public void setEncodePassword(String password) {
- PasswordEncoder encoder = new BCryptPasswordEncoder();
- String encodePasswd = encoder.encode(password);
- this.password = encodePasswd;
- }
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
- @Override
- public boolean isEnabled() {
- return true;
- }
- }
Authority.java
- package com.liuyanzhao.chuyun.entity;
- import org.springframework.security.core.GrantedAuthority;
- import javax.persistence.;
- /
- 权限
- @author 言曌
- @date 2018/1/26 下午2:05
- /
- @Entity
- public class Authority implements GrantedAuthority {
- private static final long serialVersionUID = 1L;
- @Id // 主键
- @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
- private Long id; // 用户的唯一标识
- @Column(nullable = false) // 映射为字段,值不能为空
- private String name;
- public Long getId() {
- return id;
- }
- public void setId(Long id) {
- this.id = id;
- }
- @Override
- public String getAuthority() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
四、Dao 层
UserRepository.java
- package com.liuyanzhao.chuyun.repository;
- import com.liuyanzhao.chuyun.entity.User;
- import org.springframework.data.jpa.repository.JpaRepository;
- /
- 用户repository
- @author 言曌
- @date 2017/12/27 0027 20:50
- /
- public interface UserRepository extends JpaRepository<User, Long> {
- /
- 根据用户名查找用户
- @param username
- @return
- /
- User findByUsername(String username);
- }
五、Service 层
CustomUserService.java
- package com.liuyanzhao.chuyun.service;
- import com.liuyanzhao.chuyun.entity.User;
- import com.liuyanzhao.chuyun.repository.UserRepository;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.authentication.LockedException;
- import org.springframework.security.core.userdetails.UserDetailsService;
- import org.springframework.security.core.userdetails.UsernameNotFoundException;
- import org.springframework.stereotype.Service;
- /
- @author 言曌
- @date 2018/1/30 下午8:37
- /
- @Service
- public class CustomUserService implements UserDetailsService{
- @Autowired
- private UserRepository userRepository;
- @Override
- public User loadUserByUsername(String username) throws UsernameNotFoundException {
- User user = userRepository.findByUsername(username);
- if (user == null) {
- throw new UsernameNotFoundException(“用户名不存在”);
- } else if(“locked”.equals(user.getStatus())) { //被锁定,无法登录
- throw new LockedException(“用户被锁定”);
- }
- return user;
- }
- }
六、Spring Security 配置
SecurityConfig.java
- package com.liuyanzhao.chuyun.config;
- import com.liuyanzhao.chuyun.service.CustomUserService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.security.authentication.AuthenticationProvider;
- import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
- import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
- import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
- 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.configuration.WebSecurityConfigurerAdapter;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- /
- 安全配置类
- @author 言曌
- @date 2018/1/23 上午11:37
- /
- @EnableWebSecurity
- @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法安全设置
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- private static final String KEY = “liuyanzhao.com”;
- @Autowired
- private PasswordEncoder passwordEncoder;
- @Bean
- CustomUserService customUserService() {
- return new CustomUserService();
- }
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder(); // 使用 BCrypt 加密
- }
- @Bean
- public AuthenticationProvider authenticationProvider() {
- DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
- authenticationProvider.setUserDetailsService(customUserService());
- authenticationProvider.setPasswordEncoder(passwordEncoder); // 设置密码加密方式
- return authenticationProvider;
- }
- /
- 自定义配置
- /
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests().antMatchers(“/”,“/css/”, “/js/”, “/fonts/”,“/users”).permitAll() // 都可以访问
- .antMatchers(“/h2-console/”).permitAll() // 都可以访问
- .antMatchers(“/admin/”).hasRole(“ADMIN”) // 需要相应的角色才能访问
- .antMatchers(“/console/”).hasAnyRole(“ADMIN”,“USER”) // 需要相应的角色才能访问
- .and()
- .formLogin() //基于 Form 表单登录验证
- .loginPage(“/login”).failureUrl(“/login?error=true”) // 自定义登录界面
- .and().rememberMe().key(KEY) // 启用 remember me
- .and().exceptionHandling().accessDeniedPage(“/403”); // 处理异常,拒绝访问就重定向到 403 页面
- http.csrf().ignoringAntMatchers(“/h2-console/”); // 禁用 H2 控制台的 CSRF 防护
- http.headers().frameOptions().sameOrigin(); // 允许来自同一来源的H2 控制台的请求
- }
- /*
- 认证信息管理
- @param auth
- @throws Exception
- */
- @Autowired
- public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
- //auth.userDetailsService(userDetailsService);
- auth.userDetailsService(customUserService());
- auth.authenticationProvider(authenticationProvider());
- }
- }
七、HTML 页面
登录页面:http://localhost:8080/login
登录错误页面:http://localhost:8080/login?error=true
退出登录:http://localhost:8080/logout
index.html
- <!DOCTYPE html>
- <html xmlns=”http://www.w3.org/1999/xhtml”
- xmlns:th=”http://www.thymeleaf.org”
- xmlns:sec=”http://www.thymeleaf.org/thymeleaf-extras-springsecurity4”>
- <head>
- <meta charset=“UTF-8”>
- <meta name=“viewport”
- content=“width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no”>
- </head>
- <body>
- <!–匿名–>
- <div sec:authorize=“isAnonymous()”>
- 未登录,点击 <a th:href=“@{/login}”>登录</a>
- </div>
- <!–登录–>
- <div sec:authorize=“isAuthenticated()”>
- <p>已登录</p>
- <p>登录名:<span sec:authentication=“name”></span></p>
- <p>角色:<span sec:authentication=“principal.authorities”></span></p>
- <p>Username:<span sec:authentication=“principal.username”></span></p>
- <p>Password:<span sec:authentication=“principal.password”></span></p>
- <p>Email :<span sec:authentication=“principal.email”></span></p>
- <p>Name:<span sec:authentication=“principal.name”></span></p>
- <p>Status:<span sec:authentication=“principal.status”></span></p>
- <p>拥有的角色:
- <span sec:authorize=“hasRole(‘ROLE_ADMIN’)”>管理员</span>
- <span sec:authorize=“hasRole(‘ROLE_USER’)”>用户</span>
- </p>
- </div>
- </body>
- </html>
login.html
- <!DOCTYPE html>
- <html xmlns=”http://www.w3.org/1999/xhtml”
- xmlns:th=”http://www.thymeleaf.org”>
- <head>
- <meta http-equiv=“Content-Type” content=“text/html; charset=utf-8”/>
- <meta name=“viewport” content=“width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no”>
- <title>登录页面</title>
- <body>
- <form th:action=“@{login}” method=“post” id=“loginForm”>
- <span th:if=“ {session.SPRING_SECURITY_LAST_EXCEPTION.message}”></span>
- 用户名:<input type=“text” name=“username” class=“username” id=“username” placeholder=“用户名” autocomplete=“off”/> <br>
- 密 码:<input type=“password” name=“password” class=“password” id=“password” placeholder=“密码”
- oncontextmenu=“return false”
- onpaste=“return false”/> <br>
- <input type=“checkbox” name=“remember-me”/>记住我 <br>
- <input id=“submit” type=“submit” value=“登录”/>
- </form>
- </body>
- </html>
八、运行效果
1、访问首页:http://localhost:8080