Java中Spring的Security使用详解
作者:兔老大RabbitMQ
Spring Security
在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。
Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。
特别是在spring boot项目中加入spring security更是十分简单。
本篇我们介绍spring security,以及spring security在web应用中的使用。
一个例子入门
假设我们现在创建好了一个springboot的web应用,有一个控制器如下:
@Controller public class AppController { @RequestMapping("/hello") @ResponseBody String home() { return "Hello ,spring security!"; } }
我们启动应用,假设端口是8080,那么当我们在浏览器访问//localhost:8080/hello的时候可以在浏览器看到Hello ,spring security!。
加入spring security 保护应用
此时,/hello是可以自由访问。假设,我们需要具有某个角色的用户才能访问的时候,我们可以引入spring security来进行保护。加入如下maven依赖,并重启应用:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
再次访问/hello,我们可以得到一个http-basic的认证弹窗,如下:
代码如下:
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'> <h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'> <table> <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr> <tr><td>Password:</td><td><input type='password' name='password'/></td></tr> <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr> <input name="_csrf" type="hidden" value="635780a5-6853-4fcd-ba14-77db85dbd8bd" /> </table> </form></body></html>
我们可以发现,这里有个form 。action=”/login”,这个/login是spring security提供的。
form表单提交了三个数据:
- username 用户名
- password 密码
- _csrf CSRF保护方面的内容
说明spring security 已经起作用了。这时,它为你生成了账号和密码,在内存中。
但是,我们不可能只是这么使用它,我们如何通过访问数据库来登录,验证权限呢?
接下来通过一个实例继续深入。
demo项目权限介绍
我们通过一个很简单的项目来认识一下Spring Security。
- index.html:社区首页(只有四个链接)----------任何人都可以访问
- discuss.html:帖子详情页面(只有一句话)------任何人都可以访问
- letter.html:私信列表(只有一句话)-------只有登陆后的用户才能访问
- admin.html:管理员页面(只有一句话)----只有管理员才能访问
- login.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> <ul> <li><a th:href="@{/discuss}" rel="external nofollow" >帖子详情</a></li> <li><a th:href="@{/letter}" rel="external nofollow" >私信列表</a></li> <li><a th:href="@{/loginpage}" rel="external nofollow" rel="external nofollow" >登录</a></li> <li><a th:href="@{/loginpage}" rel="external nofollow" rel="external nofollow" >退出</a></li> </ul> </body> </html>
discuss.html:帖子详情页面(只有一句话)
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>帖子</title> </head> <body> <h1>帖子详情页面</h1> </body> </html>
letter.html:私信列表(只有一句话)
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>私信</title> </head> <body> <h1>私信列表页面</h1> </body> </html>
admin.html:管理员页面(只有一句话)
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>管理员</title> </head> <body> <h1>管理员专属页面</h1> </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" action="#"> <p style="color:red;"> <!--提示信息--> </p> <p> 账号:<input type="text" > </p> <p> 密码:<input type="password" > </p> <p> 验证码:<input type="text" > </p> <p> <input type="submit" value="登录"> </p> </form> </body> </html>
service层和user的操作
首先要处理我们的用户user表,写出获取权限的方法。
实现UserDetails 接口,和接口定义的方法。
方法的作用我都已经标注在代码里。
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; 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; /* get/set方法 toSring方法 */ // 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; } // 返回用户权限 //我们有两种用户:1代表管理员,2代表普通用户 @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> list = new ArrayList<>(); list.add(new GrantedAuthority() { @Override public String getAuthority() { switch (type) { case 1: return "ADMIN"; default: return "USER"; } } }); return list; } }
着重介绍一下getAuthorities方法:
它的返回值是一个权限集合,因为我们真实开发可能是这样的:
- 用户表:记录了用户类型(是普通用户还是1级管理员、2级管理员、卖家买家等等)
- 权限表:记录了每个角色有什么权限,比如普通用户可以发帖评论点赞,管理员可以删贴置顶等等。
最后我们通过用户类型,查到多种权限,并且返回。
因为每种用户有多种权限,所以getAuthorities方法的返回值是一个权限集合。,这个集合可以装很多GrantedAuthority对象。
本代码的集合只装了一个权限对象,并且重写了对应回去权限的方法。。
service层实现接口和对应方法
import com.nowcoder.community.dao.UserMapper; import com.nowcoder.community.entity.User; 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; @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 this.findUserByName(username); } }
这个接口是要实现查找对应的用户。
我们自己写登录逻辑的时候,一样要这么做:用账号(id)查到用户,读取密码,看看用户输入的和在数据库查到的是否相同,
security底层也是做了类似的事情,所以我们需要告诉他如何查找用户。
如果我们之前写过selectByName之类的方法,可以直接调用即可。
这里我把dao层和xml实现也给出来。
import com.nowcoder.community.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper { User selectByName(String username); }
<mapper namespace="com.community.dao.UserMapper"> <sql id="selectFields"> id, username, password, salt, email, type, status, activation_code, header_url, create_time </sql> <select id="selectByName" resultType="User"> select <include refid="selectFields"></include> from user where username = #{username} </select> </mapper>
核心操作
书写配置类统一管理。
有详细的注释。
通常我们需要书写如下代码:
忽略哪些资源?
认证
授权
package com.community.config; import com.community.entity.User; import com.community.service.UserService; import com.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/**"); } // 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.equalsIgnoreCase("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); } }
- 表单
<!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>
- HomeController
package com.nowcoder.community.controller; import com.nowcoder.community.entity.User; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @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"; } }
到此这篇关于Java中Spring的Security使用详解的文章就介绍到这了,更多相关Spring的Security内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!