Java牛客项目课_仿牛客网讨论区_第七章
Posted 夜中听雪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java牛客项目课_仿牛客网讨论区_第七章相关的知识,希望对你有一定的参考价值。
文章目录
- 第七章、项目进阶,构建安全高效的企业服务
- 7.1、Spring Security(原型项目上加Spring Security,不是在真正的项目上更改)
- 7.3、权限控制(这是在真正的项目上更改,增加了SpringSecurity)
- 7.5、置顶、加精、删除
- 7.8、Redis高级数据类型(测试HyperLogLog和Bitmap用Java语言的使用,未修改项目代码)
- 7.10、网站数据统计(项目中使用:HyperLogLog和Bitmap用Java语言的使用)
- 7.13、任务执行和调度(只测试JDK 线程池、Spring 线程池、分布式定时任务 - Spring Quartz,未修改项目代码)
- 7.16、热帖排行
- 7.19、生成 长图(chang tu)
- 7.23、将文件上传至云服务器
- 7.27、优化网站的性能
第七章、项目进阶,构建安全高效的企业服务
7.1、Spring Security(原型项目上加Spring Security,不是在真正的项目上更改)
Spring Security底层用11个Filter来做权限控制之类,如果你没登录,你连DispatcherServlet都访问不了,就更不用说Controller了。
Filter和DispatcherServlet都是JavaEE的标准,是由SpringMVC实现的。Interpector和Controller是SpringMVC自己的。
老师建议研究SpringSecurity的源码,因为1、这个组件在SpringCloud、SpringBoot,即Spring家族中都能用,研究它,你会扩展的研究其他组件。2、它很复杂,在Spring家族中复杂程度靠前,是块硬骨头,建议先啃。
老师推荐这个网站Spring For All 玩最纯粹的技术!做最专业的 Spring 民间组织~学SpringSecurity:社区 Spring Security 从入门到进阶系列教程 | Spring For All
因为是中文的,而且文章质量普遍高。
研究源码要打断点跟一下流程。但不要每个源码都跟,光跟一下核心类就好。
比如SpringSecurity中的几个核心Filter。
重定向:
转发:
区别:
1、
前者是两个独立的功能,没有耦合,比如删除某帖子,然后重定向到主页,即查询所有帖子。
后者是需要两个组件实现一个请求,有耦合,例如图片中例子,登录表单提交到“/login”,然后发现登录失败,就携带错误信息转发给去模板页面的地址“/loginpage”,如果在Controller里,是可以把参数放model里直接转给模板的,但是http.formLogin().failureHandler()不在Controller里。转发比转给模板更灵活,因为可以复用“/loginpage”里的一些逻辑
2、地址栏的地址显示不同
SecurityConfig.java类
注释:
authentication,认证
authorization,授权
package com.nowcoder.community.config;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CommunityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源的访问
web.ignoring().antMatchers("/resources/**");
}
//authentication,认证
// 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);
}
});
}
//authorization,授权
@Override
protected void configure(HttpSecurity http) throws Exception
{
//super.configure(http);//覆盖该方法
// 登录相关配置
http.formLogin()
.loginPage("/loginpage")//跳转到登录页面的路径,见HomeController
.loginProcessingUrl("/login")//登录表单提交到哪个路径的Controller
//.successForwardUrl()//成功时跳转到哪里。但由于我们要除了一些逻辑,跳转时还要携带一些参数,于是使用下方的.successHandler()会更灵活。
//.failureForwardUrl()//失败时跳转到哪里。同理。
.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")//只要你拥有"USER", "ADMIN"中任何一个权限,你就可以访问私信"/letter"页面
.antMatchers("/admin").hasAnyAuthority("ADMIN")
.and().exceptionHandling().accessDeniedPage("/denied");//如果权限不匹配,就跳转到"/denied"页面
//验证码应该在账号密码处理之前先处理,如果验证码都不对,就不用看账号密码了。所以要在验证账号密码的Filter之前增加一个验证验证码的Filter验证
// 增加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.equalsIgnoreCase("1234")) {
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;//如果验证码不对,请求不会继续向下执行
}
}
// 让请求继续向下执行.
filterChain.doFilter(request, response);//验证码对了,才不会return,才会走到这里,请求才会继续向下执行.
}
}, UsernamePasswordAuthenticationFilter.class);//新的这个new Filter,要在UsernamePasswordAuthenticationFilter这个Filter之前过滤
/*
如果勾选了"记住我",Spring Security会往浏览器里存一个cookie,cookie里存着user的用户名,
然后,关掉浏览器/关机,下次再访问时,浏览器把cookie传给服务器,服务器根据用户名和userService查出该用户user,
然后,会通过SecurityContextHolder把user存入SecurityContext中,
然后,用户访问"/index"页面时,会从SecurityContext取出user的用户名,然后显示在主页上.
*/
// 记住我
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl())//如果你想把数据存到Redis/数据库里,那么就自己实现TokenRepository接口,然后.tokenRepository(tokenRepository)这样
.tokenValiditySeconds(3600 * 24)//24小时
.userDetailsService(userService);//必须有
}
}
部分index.html代码
<!--SpringSecurity规定,退出必须使用post请求。第一个<li>是get请求。第二个<li>是post请求,post请求必须使用form表单。-->
<!--<li><a th:href="@{/loginpage}">退出</a></li>-->
<li>
<form method="post" th:action="@{/logout}">
<a href="javascript:document.forms[0].submit();">退出</a>
</form>
</li>
部分HomeController.java代码
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
// 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
// return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());这是存入的东西,取出principal会取出user
Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (obj instanceof User) {
model.addAttribute("loginUser", obj);
}
return "/index";
}
login.html部分代码
<form method="post" th:action="@{/login}">
<p style="color:#ff0000;" th:text="${error}">
<!--提示信息-->
</p>
<!--name=""中的:username,password,remember-me.这三个名字是SpringSecurity固定的.-->
<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>
7.3、权限控制(这是在真正的项目上更改,增加了SpringSecurity)
牛客课程助教 V 助教 回复 Eric.Lee :
- Security提供了认证和授权两个功能,我们在DEMO里也做了演示,而在项目中应用时,我们并没有使用它的 认证功能,而单独的使用了它的授权功能,所以需要对认证的环节做一下特殊的处理,以保证授权的正常进行;
- Security的所有功能,都是基于Filter实现的,而Filter的执行早于Interceptor和Controller,关于Security的Filter原理,可以参考http://www.spring4all.com/article/458;
- 我们的解决方案是,在Interceptor中判断登录与否,然后人为的将认证结果添加到了SecurityContextHolder里。这里要注意,由于Interceptor执行晚于Filter,所以认证的进行依赖于前一次请求的Interceptor处理。比如,我登录成功了,然后请求自行重定向到了首页。在访问首页时,认证Filter其实没起作用,因为这个请求不需要权限,然后执行了Interceptor,此时才将认证结果加入SecurityContextHolder,这时你再访问/letter/list,可以成功,因为在这次请求里,Filter根据刚才的认证结果,判断出来你有了权限;
- 退出时,需要将SecurityContextHolder里面的认证结果清理掉,这样下次请求时,Filter才能正确识别用户的权限;
- LoginTicketInterceptor中的afterCompletion中其实不用清理SecurityContextHolder,将这句话删掉。
2020-01-21 10:26:54
Eric.Lee 回复 牛客课程助教 : 那对于下一次请求,Security是通过用户请求中带的cookie找到SecurityContextHolder中的保存的对应用户信息和权限的吗?
2020-01-22 17:17:14
牛客课程助教 V 助教 : SecurityContextHolder 底层默认采用Session存数据, 而Session依赖于Cookie.
2020-02-10 12:14:55
SecurityConfig.java
package com.nowcoder.community.config;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 授权
http.authorizeRequests(以上是关于Java牛客项目课_仿牛客网讨论区_第七章的主要内容,如果未能解决你的问题,请参考以下文章