发布于2021-06-12 14:55 阅读(242) 评论(0) 点赞(27) 收藏(4)
我们已经把牛客社区功能全部实现,这一章将对项目进行一些补充完善。
1.系统的安全性
2.进一步提高系统的性能
功能强大、方便扩展
认证:判断用户有无登录
如果没有登录,不可能实现 发布帖子,评论的功能。
授权: 认证之后,要判断 该用户有无访问权限,比如给帖子置顶、加精、删除,不是每个用户都能做这些事。
只有管理员、博主来可以。
servlet就是java EE。
要大致了解。spring security 底层的原理,方便之后展开学习
spring MVC的核心组件是 dispatcher Servlet,即所有请求,都要提交给dispatch Servlet进行处理,其会把请求分发给一个个控制器,由控制器 进行处理。即dispatcher Servlet 和controller是一对多的关系。
拦截器interceptor可以拦截访问controller的请求,这就是spring MVC底层的内容和框架。总结如下:
dispatcher Servlet是一个具体的Servlet接口的实现类。javaEE提出了规范——是Servlet的接口,dispatcher Servlet满足了此规范,
dispatcher Servlet即是spring MVC核心,又是java EE的规范。
java EE的规范不只是Servlet,还有一个组件——filter(过滤器),也是java EE的规范。
filter和dispatcher 之间的关系,就像拦截器和controller之间的关系。即filter可以拦截对Servlet的访问。
filter和spring MVC没有关系。
那么spring security 底层,如何利用统一的机制处理系统的各种权限呢,是利用filter组件,其底层有很多filter(大概11个),
一个filter专门验证账号密码对不对,一个filter专门做退出操作…(每个filter只做一件事)
每个filter对相应请求进行拦截,实现权限管理。
spring security 对权限控制的时机,是比较靠前的,如果没有权限,甚至不能访问dispatcher Servlet,更不必说controller
后面会演示,如何自定义一个filter代码以解决我们自定义的问题。
spring security 是spring 所有模块中最复杂的,该复杂不是体现在应用上,而是在使用上。
spring security 如果能吃透,那么spring 具体掌握了
http://www.spring4all.com/article/428
此外还有一个隐含路径admin,能够访问:
现在帖子功能是有问题的,没有登录就能访问私信列表和admin
导入porn后,重启进行测试
可以看到首先就是要登录。
可以看到,在日志里有账号密码
登录后,可以成功访问那些功能。
需要在业务层以及数据访问层,在user实体类中
user关联角色,角色关联权限。
在user中实现:
// 返回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() {//返回该用户所具备的权限
List<GrantedAuthority> list = new ArrayList<>();
list.add(new GrantedAuthority() {//集合里只装一个权限,一个用户返回一个权限
@Override
public String getAuthority() {
switch (type) {
case 1://case为 2的情况先不考虑
return "ADMIN";
default:
return "USER";
}
}
});
return list;
}
}
在UserService中:
@Service
public class UserService implements UserDetailsService {//UserDetailsService是 spring security 底层接口,检查登录的时候需要用到
@Autowired
private UserMapper userMapper;
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
//添加的部分:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查用户,和上面findUserByName方法类似。只不过名字不一样,这也是该方法为啥要写在UserService里面的原因
return this.findUserByName(username);
}
}
在SecurityConfig包下,新建:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;//因为该组件实现了UserDetailsService 接口
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源的访问
web.ignoring().antMatchers("/resources/**");//忽视resources下所有资源,都是静态资源,因为静态资源没有什么可保密的,可以随意访问,可以提高性能。
}
// AuthenticationManager: 认证的核心接口.
// AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具类.
// ProviderManager: AuthenticationManager接口的默认实现类.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 内置的认证规则(权限管理是两个方面。认证+授权)
// auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));//Pbkdf2PasswordEncoder根据sault进行加密
// 自定义认证规则,不用内置的默认规则,只不过稍微麻烦
// AuthenticationProvider: ProviderManager持有一组(一组 即多个)AuthenticationProvider,每个AuthenticationProvider负责一种认证。ProviderManager不亲自认证,而是里面的很多小弟AuthenticationProvider,每一个负责一种认证
// 上面即委托模式: ProviderManager将认证委托给AuthenticationProvider.
auth.authenticationProvider(new AuthenticationProvider() {//传入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("密码不正确!");
}
// UsernamePasswordAuthenticationToken中 principal: 认证的主要信息,一般是user; credentials: 证书,在账号密码模式下是密码; authorities: 权限;
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());//返回认证结果, user.getAuthorities()当前用户的权限
}
// 当前的AuthenticationProvider支持哪种类型的认证.
@Override
public boolean supports(Class<?> aClass) {
// UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.代表账号密码验证
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//避开默认登录页面,整我们自己的登录页面——登录相关配置,告诉security我们的登录界面
http.formLogin()
.loginPage("/loginpage")
.loginProcessingUrl("/login")
.successHandler(new AuthenticationSuccessHandler() {//AuthenticationSuccessHandler成功时接口
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath() + "/index");//重定向到首页
}
})
.failureHandler(new AuthenticationFailureHandler() {//AuthenticationSuccessHandler失败时接口
@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");//跳转至(重定向)首页
}
});
@Controller
public class HomeController {
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
// 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.(通过底层的filte实现)
Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();//SecurityContextHolder.getContext()得到SecurityContext对象,其中存着认证结果即.getAuthentication(),通过getPrincipal得到主要信息
if (obj instanceof User) {//如果确实是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";
}
}
}
重定向
浏览器访问 服务器中的A组件,A组件没有结果返回。例如,访问删除,无需返回。但是需要给浏览器显示一些内容,
例如B组件是查询,删除以后要返回查询页面,两个独立组件之间的跳转,适合用重定向。
下图这样a直接调用b,容易产生耦合,而重定向 可以降低耦合度。
重定向:
两个组件之间若想共享数据,只能通过 cookie,session
还有一种,A只能处理请求的一半,剩下一半需要b处理,由b处理剩下的,并返回服务器,这样a和b可以通过request进行数据共享(整个过程是一个请求(来自浏览器),a给b的时候,要把request和response给b)
在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>
在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}"><!--action是路径-->
<a href="javascript:document.forms[0].submit();">退出</a> <!--退出必须是post请求,而不是get,不符合要求--> <!--退出是提交表单,所以不方便写链接,写一段简单的js-->
</form>
</li>
</ul>
</body>
</html>
测试:
登录成功:
访问不了admin
退出以后 访问不了私信
验证码、记住我两个功能的实现:
验证码是在账号密码之前验证的,验证码都不对的话,无需验证账号密码。
因此在账号密码验证之前,增加一个filter
// 增加Filter,处理验证码
http.addFilterBefore(new Filter() {//第一个参数new Filter()表示实例化一个Filter,第二个参数表示在哪个Filter之前新加Filter。第137行
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;//servletRequest是HttpServletRequest的父接口,向下转型必须强制转型
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getServletPath().equals("/login")) {//判断是否为登录请求,只有登录请求才需要处理验证码逻辑
String verifyCode = request.getParameter("verifyCode");
if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {//验证码为空或者不为1234
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);//验证码错误 需要回到登录页面
return;
}
}
// 让请求继续向下执行.走到下一个Filter,如果下面没有Filter,则走到servlet。//如果没有这句话,请求到此终止,不会在往下走
filterChain.doFilter(request, response);
}
}, UsernamePasswordAuthenticationFilter.class);//在此Filter之前新加Filter
}
}
// 记住我
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl())//默认接口
.tokenValiditySeconds(3600 * 24)//过期时间,单位s,24h,过了这个时间就不记住了
.userDetailsService(userService);//要记住的信息userDetailsService,靠此信息通过凭证。
在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"> 记住我<!-- checkbox 表示勾选框 -->
</p>
<p>
<input type="submit" value="登录">
</p>
</form>
</body>
</html>
测试:
当选中“记住我”功能后
当我们关掉浏览器,再次打开网站后,不需要登录。帮我们绕过了认证的过程。
作者:忽明忽灭
链接:http://www.javaheidong.com/blog/article/222520/d7563039562450c4cf1b/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!