Demo素材来源于牛客网
注意:SpringBoot2.3.x 大版本统一最好,以免 bug 挡路!
1. Demo 环境搭建
1.1 数据库sql
1.1.1 新建数据
新建一个名为community 的数据库,编码类型 utf8mb4
1.1.2 user表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`salt` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`type` int(11) NULL DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;',
`status` int(11) NULL DEFAULT NULL COMMENT '0-未激活; 1-已激活;',
`activation_code` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`header_url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `index_username`(`username`(20)) USING BTREE,
INDEX `index_email`(`email`(20)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 156 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
复制代码
1.2 初始项目导入
初始项目下载下来,然后使用IDEA 将其导入即可!(如果链接失效,请评论留言,我会更新的!)
- 链接:pan.baidu.com/s/1CQ9rDbpo…
- 提取码:
3gyt
Demo 项目结构如图所示:
2. SpringBoot 整合Spring Security
2.1 pom.xml 引入依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
复制代码
依赖我们可以去Maven 远程仓库中搜索:
当我们引入依赖后,SpringSecurity 就立即生效!启动项目主函数后,我们来测试访问下首页:http://localhost:8080/community/index,这时候它会自动被拦截到登SpringSecurity自带的登录页面:
SpringSecurity默认提供的登录页面,账号默认是user
,密码在我们启动项目主函数时,会以日志的形式打印在控制台:
输入账号密码登录后,就能进入我们项目的首页 inedx.html
问题:那么如何使用自己自定义的登录页面以及自己数据中的用户名和密码来控制登录权限呢?接下来就带着这个问题一步一步往下走!
2.2 对User实体进行加功处理
我们使用Lombok 插件的注解简化 setter/getter 以及构造函数和 toString 方法,还有个好处就是可以更清晰的展示实现 UserDetails 接口后需要重写的方法!
/**
* @Auther: csp1999
* @Date: 2020/12/02/17:29
* @Description:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ToString
public class User implements UserDetails {
private int id;
private String username;
private String password;
private String salt;
private String email;
private int type;
private int status;
private String activationCode;
private String headerUrl;
private Date createTime;
/**
* 获取某个用户所具备的权限的集合
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> list = new ArrayList<>();
// 我们demo中只给用户加一个权限
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
switch (type) {
case 1:
return "ADMIM";// 管理员
default:
return "USER";// 普通与用户
}
}
});
return list;
}
/**
* 返回true: 账号未过期.
* 返回false: 账号过期.
*
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 返回true: 账号未锁定.
* 返回false: 账号锁定.
*
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 返回true: 凭证未过期.
* 返回false: 凭证过期.
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 返回true: 账号可用.
* 返回false: 账号不可用.
*
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
复制代码
2.3 对UserService 实现类进行加功处理
/**
* @Auther: csp1999
* @Date: 2020/12/02/17:31
* @Description:
*/
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 我们自己写的根据用户名从数据库查询用户信息
* @param username
* @return
*/
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
/**
* UserDetailsService 接口提供的根据用户名获取用户信息的方法
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用我们自己的findUserByName 方法
return this.findUserByName(username);
}
}
复制代码
2.4 SecurityConfig 配置类
/**
* @Auther: csp1999
* @Date: 2020/12/02/18:41
* @Description: Spring Security 配置类
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
/**
* 用于对认证进行处理(核心)
* <p>
* AuthenticationManager: 用于认证的核心接口.
* AuthenticationManagerBuilder: 用于构建AuthenticationManager接口对象的工具.
* ProviderManager: AuthenticationManager接口的默认实现类.
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 内置(默认)的认证规则:
// auth.userDetailsService(userService)
// 对密码进行编码,Pbkdf2PasswordEncoder 加密时候附带的盐值
// .passwordEncoder(new Pbkdf2PasswordEncoder("securtyu"));
/**
* 自定义认证规则:
* AuthenticationProvider: ProviderManager持有一组 AuthenticationProvider,
* 每个 AuthenticationProvider 负责一种认证.
*
* 这种设计模式称为委托模式: ProviderManager 将认证委托给 AuthenticationProvider.
*/
auth.authenticationProvider(new AuthenticationProvider() {
/**
* Authentication: 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息.
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取用户名
String username = authentication.getName();
// 获取密码
String password = (String) authentication.getCredentials();
User user = userService.findUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("账号不存在!");
}
// 对密码加密后再到数据库查询
password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
throw new BadCredentialsException("密码不正确!");
}
// principal: 认证的主要信息(比如user对象);
// credentials: 证书(账号密码模式下,证书使用密码password表示);
// authorities: 权限;
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
/**
* 当前的 AuthenticationProvider支持的是哪种类型的认证.
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
// 支持账号密码认证模式:
// UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
});
}
/**
* 用于对授权进行处理(核心)
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 父类授权逻辑,想深入了解,点入super查看其源码
// super.configure(http);
// 登录页面相关配置
http.formLogin()
// 指定登录页面
.loginPage("/loginpage")
// 处理登录请求的路径
.loginProcessingUrl("/login")
// 登录成功时候跳转的路径
// .successForwardUrl("/xxx")
// 登录失败时跳转的路径
// .failureForwardUrl("/xxx")
// 登录成功后,做出相应处理的 handler
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
// 重定向到首页
response.sendRedirect(request.getContextPath() + "/index");
}
})
// 登录失败后,做出相应处理的 handler
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
// 储存错误信息
request.setAttribute("error", exception.getMessage());
// 转发到登录页面
request.getRequestDispatcher("/loginpage").forward(request, response);
}
});
// 退出相关配置
http.logout()
// 处理登出的请求
.logoutUrl("/logout")
// 登出后,做出相应处理的 handler
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
// 重定向到首页
response.sendRedirect(request.getContextPath() + "/index");
}
});
// 授权配置
http.authorizeRequests()
.antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
.antMatchers("/admin").hasAnyAuthority("ADMIN")
// 如果没有权限,则访问 denied 提示页面
.and().exceptionHandling().accessDeniedPage("/denied");
// 增加一个Filter,用于处理验证码
http.addFilterBefore(new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getServletPath().equals("/login")) {
String verifyCode = request.getParameter("verifyCode");
// 这里把验证码校验写死,实际业务中是需要动态的验证码:
// 需要加一个验证码生成的方法,把验证码存入 cookie 或者 redis 中
// 这里仅仅是为了展示效果而已:
if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;
}
}
// 让请求继续向下执行.
filterChain.doFilter(request, response);
}
// 新增的这个过滤器new Filter()会在 UsernamePasswordAuthenticationFilter 过滤器之前执行
}, UsernamePasswordAuthenticationFilter.class);
// 记住我
http.rememberMe()
// 存储用户数据的方案: new InMemoryTokenRepositoryImpl() 把用户数据存入内存
// 如果是存cookie 或者 redis 存储token 需要自己写相关实现方法,并在这里new 出方法实例
.tokenRepository(new InMemoryTokenRepositoryImpl())
// 过期时间 24h
.tokenValiditySeconds(3600 * 24)
// 指定 userService
.userDetailsService(userService);
}
/**
* 用于配置一些拦截的资源
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略resources 下的所有静态资源
web.ignoring().antMatchers("/resources/**");
}
}
复制代码
2.5 controller 层修改
/**
* @Auther: csp1999
* @Date: 2020/12/02/17:28
* @Description:
*/
@Controller
public class HomeController {
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
// 认证成功后,结果user信息会通过SecurityContextHolder存入SecurityContext中.
Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (obj instanceof User) {
model.addAttribute("loginUser", obj);
}
return "/index";
}
@RequestMapping(path = "/discuss", method = RequestMethod.GET)
public String getDiscussPage() {
return "/site/discuss";
}
@RequestMapping(path = "/letter", method = RequestMethod.GET)
public String getLetterPage() {
return "/site/letter";
}
@RequestMapping(path = "/admin", method = RequestMethod.GET)
public String getAdminPage() {
return "/site/admin";
}
@RequestMapping(path = "/loginpage", method = {RequestMethod.GET, RequestMethod.POST})
public String getLoginPage() {
return "/site/login";
}
// 拒绝访问时的提示页面
@RequestMapping(path = "/denied", method = RequestMethod.GET)
public String getDeniedPage() {
return "/error/404";
}
}
复制代码
2.6 html页面修改
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>社区首页</h1>
<!--欢迎信息-->
<p th:if="${loginUser!=null}">
欢迎你, <span th:text="${loginUser.username}"></span>!
</p>
<ul>
<li><a th:href="@{/discuss}">帖子详情</a></li>
<li><a th:href="@{/letter}">私信列表</a></li>
<li><a th:href="@{/loginpage}">登录</a></li>
<!--<li><a th:href="@{/loginpage}">退出</a></li>-->
<li>
<form method="post" th:action="@{/logout}">
<a href="javascript:document.forms[0].submit();">退出</a>
</form>
</li>
</ul>
</body>
</html>
复制代码
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录社区</h1>
<form method="post" th:action="@{/login}">
<p style="color:red;" th:text="${error}">
<!--提示信息-->
</p>
<p>
账号:<input type="text" name="username" th:value="${param.username}">
</p>
<p>
密码:<input type="password" name="password" th:value="${param.password}">
</p>
<p>
验证码:<input type="text" name="verifyCode"> <i>1234</i>
</p>
<p>
<input type="checkbox" name="remember-me"> 记住我
</p>
<p>
<input type="submit" value="登录">
</p>
</form>
</body>
</html>
复制代码
3. 测试效果
3.1 访问首页
http://localhost:8080/community/index
试一试未登录直接访问帖子详情和私信列表页面:
可以看出,没有权限,被拦截到登录页面,然后我们登录一下:
接着,在来测试进入帖子详情和私信列表页面:
拥有权限,可以进入!
如果文章对您有帮助,点赞支持一下,之后会更新一篇关于牛客论坛的项目实战,实践一下SpringSecurity的使用~