------------------------------------
学习Springboot的时候因为要加上一些权限控制等操作,所以捣鼓了一下SpringBoot Security。记录一下。
------------------------------------
涉及的相关类:
WebSecurityConfig:Spring Security配置类。
MyPasswordEncoder:Spring security 5.0中新增了多种加密方式,也改变了密码的格式,为了更安全,具体可以在网上搜索这个错误报错进行详细了解:There is no PasswordEncoder mapped for the id “null”。在此处我是自己定义了这个密码验证方式替换掉了security的原先密码验证,因为暂时我这边用百度的答案解决不了上面我说的这个报错。
AuthSuccessHandler:登录成功后的处理类。
UserDetailsImpl:此类实现 UserDetails接口,用于进行用户验证,相当于security体系里的用户类。
UserService:此类实现 UserDetailsService,当执行登录操作之后,security会利用接口的 loadUserByUsername方法根据传进来的用户名查找对应的用户信息,然后组装成一个新的 UserDetailsImpl 提交给security 进行信息验证。
UserUtil:自定义的用户工具类,目前用于获取当前用户和获取用户是否有指定权限。
User:自定义用户类,是数据库用户表的实体类,其实UserDetailsImpl 可以继承此类更加方便,不过此处我分开来写了。
Role:自定义角色类,是数据库角色表的实体类。
UserRole:自定义用户角色关联类,是数据库用户角色关联表的实体类。
Func:自定义相关功能权限的类,是数据库功能权限表的实体类。
RoleFunc:自定义角色权限关联类,是数据库角色权限关联的实体类。
此外还有以上相关实体类的5个 Repository 接口以及部分Service类。
------------------------------------
1.首先配置一个config类继承 WebSecurityConfigurerAdapter,重写里面的几个方法以配置登录登出和权限控制。
package com.didispace;
import com.didispace.security.AuthSuccessHandler;
import com.didispace.security.MyPasswordEncoder;
import com.didispace.service.UserService;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public static MyPasswordEncoder passwordEncoder() {
return new MyPasswordEncoder();
}
@Autowired
private AuthSuccessHandler authSuccessHandler;
@Autowired
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
// 配置访问那些资源不用进行验证,在此处配置是会直接绕开spring security的所有filter,直接跳过验证
web.ignoring().antMatchers("/images/**","/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.csrf().disable(); // 一般来说security自动开启防御csrf,类似那种ajax post请求就访问不了,可以调用disable方法注销调,当然这不安全。
http
.authorizeRequests()
// http.permitAll不会绕开springsecurity验证,相当于是允许该路径通过
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 配置登录页面的访问路径
.loginPage("/login")
// 登录路径允许不用验证即可访问
.permitAll()
// 配置登录验证成功后的处理类/方法
.successHandler(authSuccessHandler)
.and()
// Spring Security已经有登出的方法,这里配置登出也是可以直接访问
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
// 这里配置security校验的方式和校验需要的一些组件
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}
2.AuthSuccessHandler是我自定义的security验证成功后的处理类,负责登录成功后重定向到指定的页面以及加载角色权限,将权限相关的信息保存到security控制的用户类里面。方法的参数 authentication 就是security用于维护用户验证信息的类,里面维护着上述的UserDetailsImpl对象。
package com.didispace.security;
import com.didispace.domain.UserDetailsImpl;
import com.didispace.service.FuncService;
import com.didispace.util.UserUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录验证成功后的处理类
*/
@Component
public class AuthSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private FuncService funcService;
@Autowired
private UserUtil userUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
System.out.println(" success !! ");
UserDetailsImpl userDetails = (UserDetailsImpl) principal;
// 从数据库找到该用户相关的所有权限并存到 security 维护的用户类里面
userDetails.setFunctionCodes(funcService.findFunctionCodesByRoleIds(userDetails.getUsername()));
}
// 将用于获取用户权限的方法/类存储到session里面,方便 html 调用方法来判断当前用户是否有权限进行操作
httpServletRequest.getSession().setAttribute("userUtil",userUtil);
// 重定向到指定的页面
httpServletResponse.sendRedirect("hello");
}
}
3.UserService实现 UserDetailsService 接口,要实现其中的 loadUserByUsername方法,如同我在上面简介说的功用。然后在config类的security验证方式配置中加入此实现类的对象。
package com.didispace.service;
import com.didispace.domain.User;
import com.didispace.domain.UserDetailsImpl;
import com.didispace.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.ArrayList;
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUserName(s);
if (user == null) {
throw new UsernameNotFoundException("没有此用户");
}
System.out.println("user_name : " + user.getUserName());
System.out.println("pass_word : " + user.getPassWord());
// 创建UserDetails的实现类,第三个参数未空列表在UserDetailsImpl里面可以看到
return new UserDetailsImpl(user.getUserName(),user.getPassWord(), new ArrayList<>());
}
}
4.再来看看security维护的用户类 UserDetailsImpl ,实现 UserDetails接口,普遍做法是此类也继承 User实体类。实现UserDetails,会要求实现其中的几个方法,比如 getPassword 和 getUsername,在实现的时候返回类中的关于用户名和密码的字段即可,hasFunctionCode是给 UserUtil 的 hasFunction方法调用的,用以html页面判断是否有相关权限,权限资料在调用AuthSuccessHandler的方法的时候就set进去了,下图还有几个需要实现的方法用默认实现或者网上查查即可。
package com.didispace.domain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class UserDetailsImpl implements UserDetails {
private String userName;
private String passWord;
private List<String> functionCodes;
public List<String> getFunctionCodes() {
return functionCodes;
}
public void setFunctionCodes(List<String> functionCodes) {
this.functionCodes = functionCodes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String functionCode : functionCodes) {
authorities.add(new SimpleGrantedAuthority(functionCode));
}
return authorities;
}
public UserDetailsImpl(String userName, String passWord, List<String> functionCodes) {
this.userName = userName;
this.passWord = passWord;
this.functionCodes = functionCodes;
}
public boolean hasFunctionCode(String... funcCode) {
for(String code :funcCode){
for(String func:getFunctionCodes()){
if(func != null && func.toString().equals(code)){
return true;
}
}
}
return false;
}
@Override
public String getPassword() {
return this.passWord;
}
@Override
public String getUsername() {
return this.userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
此外我们还可以看到有个 getAuthorities的实现方法,这个是security默认机制处理权限用的,在上面我们可以看到我们自己自定义一些方法例如UserUtil 的来获取权限信息,但其实也可以在UserUtil 中执行getAuthorities方法获取权限信息,同理只要在AuthSuccessHandler的方法的时候就set进去相关资料就行。关于这个security方法或security的权限控制的详细信息推荐大家看一下地址:
https://www.cnblogs.com/longfurcat/p/9417422.html
5.接下来看看UserUtil,hasFunction如上所说给前端页面调用,此类在AuthSuccessHandler的方法的时候就set进去,getCurrUser方法可以获取当前用户:
import com.didispace.domain.UserDetailsImpl;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
public class UserUtil {
public static UserDetailsImpl getCurrUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
if (authentication instanceof AnonymousAuthenticationToken) {
return null;
}
UserDetailsImpl user =
(UserDetailsImpl) (authentication.getPrincipal() == null ? null : authentication.getPrincipal());
return user;
}
public boolean hasFunction(String functionCode) {
return getCurrUser().hasFunctionCode(functionCode);
}
}
6.看看自定义的密码加密和配对的类,encode方法应该是加密传进来的密码用的,而matches是用 UserDetailsService接口的loadUserByUsername 方法中查出来的密码和登录页面输入进来的密码进行匹配。网上可以搜到其他的框架的加密方式,例如根据我在开头说的那个报错去查找相关信息。
package com.didispace.security;
import org.springframework.security.crypto.password.PasswordEncoder;
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
System.out.println("chars encode: " + charSequence.toString());
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
System.out.println("chars : " + charSequence.toString());
System.out.println("s : " + s);
return s.equals(charSequence.toString());
}
}
7.接下来是几个用户角色的表和实体类,有过经验的都应该知道怎么设计,我的如下:
User实体类:
package com.didispace.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User {
@Id
@Column(name = "USER_ID")
private String userId;
@Column(name = "USER_NAME")
private String userName;
@Column(name = "PASSWORD")
private String passWord;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
}
Role类:
package com.didispace.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "SYS_ROLE")
public class Role {
@Id
@Column(name = "ROLE_ID")
private String roleId;
@Column(name = "ROLE_NAME")
private String roleName;
@Column(name = "ORDER")
private Integer order;
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Integer getOrder() {
return order;
}
public void setOrder(Integer order) {
this.order = order;
}
}
UserRole类:
package com.didispace.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "USER_ROLE")
public class UserRole {
@Id
@Column(name = "ID")
private String id;
@Column(name = "USER_ID")
private String userId;
@Column(name = "ROLE_ID")
private String roleId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Func(权限)类:
package com.didispace.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "SYS_FUNC")
public class Func {
@Id
@Column(name = "FUNCTION_ID")
private String functionId;
@Column(name = "FUNCTION_NAME")
private String functionName;
@Column(name = "ORDER")
private Integer order;
@Column(name = "FUNCTION_URL")
private String functionUrl;
public String getFunctionId() {
return functionId;
}
public void setFunctionId(String functionId) {
this.functionId = functionId;
}
public String getFunctionName() {
return functionName;
}
public void setFunctionName(String functionName) {
this.functionName = functionName;
}
public Integer getOrder() {
return order;
}
public void setOrder(Integer order) {
this.order = order;
}
public String getFunctionUrl() {
return functionUrl;
}
public void setFunctionUrl(String functionUrl) {
this.functionUrl = functionUrl;
}
}
RoleFun类:
package com.didispace.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "ROLE_FUNC")
public class RoleFunc {
@Id
@Column(name = "ID")
private String id;
@Column(name = "ROLE_ID")
private String roleId;
@Column(name = "FUNC_ID")
private String funcId;
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public String getFuncId() {
return funcId;
}
public void setFuncId(String funcId) {
this.funcId = funcId;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
8.下面是权限的Service类,用以实现上述代码中的某些方法:
FuncService:
package com.didispace.service;
import com.didispace.domain.Func;
import com.didispace.domain.RoleFunc;
import com.didispace.domain.User;
import com.didispace.domain.UserRole;
import com.didispace.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class FuncService {
@Autowired
private FuncRepository funcRepository;
@Autowired
private RoleFuncRepository roleFuncRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@Autowired
private UserRepository userRepository;
public Func getFunByUrl(String url) {
Func func = funcRepository.findByFunctionUrl(url);
return func;
}
public List<String> findFunctionCodesByRoleIds(String userName) {
User user = userRepository.findByUserName(userName);
List<UserRole> userRoles = userRoleRepository.findAllByUserId(user.getUserId());
List<String> roleIds = new ArrayList<>();
for (int i=0;i<userRoles.size();i++) {
roleIds.add(userRoles.get(i).getRoleId());
}
List<RoleFunc> roleFuncs = roleFuncRepository.findDistinctByRoleId(roleIds);
List<String> codes = new ArrayList<>();
for (RoleFunc roleFunc:roleFuncs) {
codes.add(roleFunc.getFuncId());
}
return codes;
}
}
9.然后接下来就看页面啦,先是Controller:
package com.didispace.web;
import com.didispace.util.UserUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest;
@Controller
public class HelloController {
@RequestMapping("/")
public String index() {
return "index";
}
@RequestMapping("/hello")
public String hello(HttpServletRequest request) {
request.setAttribute("remoteUser", UserUtil.getCurrUser());
return "hello";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "login";
}
@RequestMapping("/helloOne")
public String helloOne() {
return "hello12";
}
}
还有个用来试试Ajax的Controller:
package com.didispace.web;
import com.didispace.model.AjaxResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestAjaxController {
@RequestMapping("/getAjaxMessage")
public AjaxResult getAjaxMessage() {
return new AjaxResult("測試","0000");
}
}
package com.didispace.model;
public class AjaxResult {
private String message;
private String code;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public AjaxResult(String message, String code) {
this.message = message;
this.code = code;
}
}
10.接下来就是Html页面,用的 thymeleaf 模板引擎:
打招呼的index界面:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security入门</title>
</head>
<body>
<h1>欢迎使用Spring Security!</h1>
<p>点击 <a th:href="@{/hello}">这里</a> 打个招呼吧</p>
</body>
</html>
先来看login页面:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
用户名或密码错
</div>
<div th:if="${param.logout}">
您已注销成功
</div>
<form th:action="@{/login}" method="post">
<div><label> 用户名 : <input type="text" name="username"/> </label></div>
<div><label> 密 码 : <input type="password" name="password"/> </label></div>
<div><input type="submit" value="登录"/></div>
</form>
</body>
</html>
然后是登录成功后重定向的hello页面:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="注销"/>
</form>
</body>
</html>
接下来说一下Ajax的页面,在config类配置的时候,如果没有配置 csrf().disable()的话,security的csrf防护是开启的,所以ajax发不过去,我查了下大家可以看看这个链接,我下面的html就是用这个方法:
https://blog.csdn.net/sinat_28454173/article/details/52251004
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>Title</title>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
<!--<script type="text/javascript" th:src="@{/js/jquery-1.10.2.js}"></script>-->
<script type="text/javascript" th:src="@{/js/min/jquery-1.12.4.min.js}"></script>
<!--<script type="text/javascript" src="../../js/index.js}"></script>-->
<script type="text/javascript">var context_path = '..';</script>
<script type="text/javascript">
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
function getAjaxMessage() {
alert("123");
$.ajax({
url: "getAjaxMessage",
type:"post",
datatype: "json",
beforeSend : function(xhr) {
xhr.setRequestHeader(header, token);
},
success: function(result) {
alert(result.message);
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
alert(textStatus + " " + XMLHttpRequest.readyState + " "
+ XMLHttpRequest.status + " " + errorThrown);
}
});
}
</script>
<body>
<h1> hello welcome !! </h1>
<span id="ajaxMessage"></span>
<input th:if="${session.userUtil.hasFunction('FUNC_HELLO')}" type="button" onclick="getAjaxMessage()" />
</body>
</html>
事实上我也试过这个链接
http://www.cnblogs.com/yjmyzz/p/customize-CsrfFilter-to-ignore-certain-post-http-request.html
中说到的自定义一个 RequestMatcher,具体是这样:
http.csrf().disable().requestMatcher(new CsrfSecurityRequestMatcher());
package com.didispace.security;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class CsrfSecurityRequestMatcher implements RequestMatcher {
private List<String> execludeUrls = new ArrayList<>();
private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
@Override
public boolean matches(HttpServletRequest httpServletRequest) {
System.out.println(" come to match !!");
if (!execludeUrls.contains("/helloOne")) {
execludeUrls.add("/helloOne");
execludeUrls.add("/getAjaxMessage");
}
if (execludeUrls != null && execludeUrls.size() > 0) {
String servletPath = httpServletRequest.getServletPath();
for (String url : execludeUrls) {
if (servletPath.contains(url)) {
return false;
}
}
}
return !allowedMethods.matcher(httpServletRequest.getMethod()).matches();
}
public List<String> getExecludeUrls() {
return execludeUrls;
}
public void setExecludeUrls(List<String> execludeUrls) {
this.execludeUrls = execludeUrls;
}
}
return false 就是允许通过了。但是配了之后发现请求绕过了 security不用登录验证都能访问,我就觉得应该缺了某些东西,可能在security里面match之前登录验证的逻辑是存在的,我自定义这个类后应该导致验证逻辑缺少了,具体还要研究一下怎么用才行。
11.最后上一下appsetting和pom文件:
server.port=8080
server.servlet.context-path=/test
spring.resources.static-locations=classpath:/static/
spring.mvc.static-path-pattern=/**
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</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-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
好了,简单的security就到此,大家有兴趣可以看看spring的官方文档研究一下。