仿牛客网社区开发——第7章 项目进阶,构建安全高效的企业服务
作者:互联网
Spring Security
简介
Spring Security 是一个专注于为 Java 应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义的需求。
特征
- 对身份的认证和授权提供全面的、可扩展的支持
- 防止各种攻击,如会话固定攻击、点击劫持、csrf 攻击等
- 支持与 Servlet API、Spring MVC 等 Web 技术集成
原理
官网:https://spring.io/projects/spring-security
中文学习网址:http://www.spring4all.com/article/428
简要来说,Spring Security 底层是利用的 Java EE 的规范。底层用了很多的 Filter,不同的 Filter 负责不同的功能。
引入 Spring Security 的依赖
引入包之后,Spring Security 权限就会生效 ,会生成随机密码,访问资源时会被拦截。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
改造 User 实体类以及 UserService
修改 User 类
- 实现 UserDetails 接口
- 重写几个方法,getAuthorities 返回用户权限,其它方法默认都返回 true
- 一个用户可能有多个权限,但这里一个用户只需要一个权限即可,所以 getAuthorities 就不需要返回集合
public class User implements UserDetails {
……
// true:账号未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// true:账号未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// true:凭证未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// true:账号可用
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> list = new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
switch (type) {
case 1:
return "ADMIN";
default:
return "USER";
}
}
});
return list;
}
}
修改 UserService 类
Spring Security 需要依赖 UserDetailsService 接口查询用户,所以需要在 UserService 实现此接口,并且实现其方法,直接调用 UserService 的查询方法
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return findUserByName(username);
}
}
编写 Security 配置类
- 前两个 configure 方法主要是认证的逻辑:
- 静态资源不需要拦截
- 不采用内置的认证规则,采用自定义认证规则。因为之前用的不是这个 Encoder,并且也不是这个固定的 salt,所以这个逻辑和我们目前的系统的现状不匹配
- 自定义认证规则中,每个 AuthenticationProvider 负责一种认证,例如有账号密码认证、QQ 认证、微信认证等等。ProviderManager 持有一组 AuthenticationProvider,ProviderManager 将认证委托给 AuthenticationProvider
- 账号不存在或密码不正确,抛出各自对应的异常
- 返回时填上 3 个对应的参数
- supports 方法中返回当前支持的认证类型
- 第三个 configure 方法:
- 进行登录和退出的相关配置:主要是路径、成功和失败的处理。
- 授权配置:每个路径能有哪些权限可以访问、拒绝访问的跳转路径
- 验证码配置:在账号密码的 Filter 之前;如果路径为 /login,且验证码错误,则拦截;否则应当放行,必须写 doFilter
- 记住我的配置:凭证存放到何处、有效时间,以及配置 userService。这里当下次访问这个网站的时候,它从内存里能根据你的凭证得到你的用户名,然后得查出用户的完整信息。怎么查呢,用这个 userService 来查,所以你得告诉它
- 另外注意各个地方是请求转发还是重定向
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源的访问
web.ignoring().antMatchers("/resources/**");
}
// AuthenticationManager:认证的核心接口
// AuthenticationManagerBuilder:用于构建AuthenticationManager对象的工具
// ProviderManager:AuthenticationManager接口的默认实现类
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 内置的认证规则
//auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));
// 自定义认证规则
// AuthenticationProvider:ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证
// 委托模式:ProviderManager将认证委托给AuthenticationProvider
auth.authenticationProvider(new AuthenticationProvider() {
// Authentication:用于封装认证信息的接口,不同的实现类代表不同类型的认证信息
@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:主要信息;credentials:证书;authorities:权限;
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
// 当前的AuthenticationProvider支持哪种类型的认证
@Override
public boolean supports(Class<?> aClass) {
// UsernamePasswordAuthenticationToken:Authentication接口的常用的实现类
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录相关配置
http.formLogin()
.loginPage("/loginpage")
.loginProcessingUrl("/login")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath() + "/index");
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
request.setAttribute("error", e.getMessage());
request.getRequestDispatcher("/loginpage").forward(request, response);
}
});
// 退出相关配置
http.logout()
.logoutUrl("/logout")
.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")
.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");
if (verifyCode == null || !verifyCode.equals("1234")) {
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;
}
}
// 让请求继续向下执行
filterChain.doFilter(request, response);
}
}, UsernamePasswordAuthenticationFilter.class);
// 记住我
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl())
.tokenValiditySeconds(3600 * 24)
.userDetailsService(userService);
}
}
HomeController
尤其注意 /index 方法,如何获取到之前存的用户信息。认证成功后,结果会通过 SecurityContextHolder 存入 SecurityContext 中
@Controller
public class HomeController {
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
// 认证成功后,结果会通过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";
}
}
前端页面
index.html
Spring Security 要求退出得是 post 请求,这里改成表单,注意超链接如何起提交的效果
<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="@{/logout}">退出</a></li>-->
<li>
<form method="post" th:action="@{/logout}">
<a href="javascript:document.forms[0].submit();">退出</a>
</form>
</li>
</ul>
login.html
记住我的 name 固定,一定要叫 remember-me
<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>
标签:return,进阶,request,认证,客网,Override,仿牛,public,String 来源: https://www.cnblogs.com/CWZhou/p/16463262.html