前言:
Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
原理就是:创建很多个filter和interceptor来进行请求的验证和拦截,以此来达到安全的效果。
下面通过一些案例演示:
一、基本案例的使用
1、基本案例功能概述
通过内存配置的用户名和密码,进行认证授权,另外配置链接/admin/**,需要admin的角色才可以访问,/user/**,需要user的角色才可以访问,案例的java类就一个主配置类,进行了功能的配置,因为springsecurity有默认的登陆/login,因此Controller主要创建了/admin/hello、/user/hello、/hello的三个请求方法。
2、代码
添加pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置类
通过configure(AuthenticationManagerBuilder) 添加认证需要提供的用户
例 auth.inMemoryAuthentication().withUser("admin") .password("admin") .roles("admin", "user") // 内存用户admin,密码admin,角色admin,user
通过configure(HttpSecurity http)进行路径对应权限的拦截以及其他的一些操作。
例 and().logout().logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandlers()) // 增加自定义退出的处理。
authorizeRequests()
.antMatchers("/user/**").hasRole("user") // user开头需要user的角色
antMatchers("/admin/**").hasRole("admin") // amdin开头需要admin的角色
and().anyRequest().authenticated() // 所有的路径需要登陆
and().csrf().disable() // 关闭默认的csrf功能,不然logout无法用get请求,以及其他类似的问题出现。
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// 添加一个admin/admin role是 admin user user/user role是user, guest, role 是guest 的静态用户数据
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin")
.password("admin")
.roles("admin", "user")
.and().withUser("user")
.password("user").roles("user").and().withUser("guest")
.password("guest").roles();
}
// 配置路径的拦截
// produect 需要user权限
// admin 需要admin权限
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("user") // user开头需要user的角色
.antMatchers("/admin/**").hasRole("admin") // amdin开头需要admin的角色
.anyRequest().authenticated() // 所有请求需要认证
.and().formLogin()
.and()
.httpBasic()
.and()
.logout() // 增加退出,指定路径是logout,退出处理结果在下面的内部类中。
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandlers())
.and().csrf().disable(); // 这里关闭csrf是为了logout生效(默认post)
}
private static class LogoutSuccessHandlers implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("退出登陆");
out.flush();
}
}
}
Controller
/**
* 如果没有配置configure(AuthenticationManagerBuilder auth),默认密码:user , 启动时随机密码
*/
@RestController
public class HelloController {
@RequestMapping("/admin/hello")
public String adminhello() {
return "admin hello";
}
@RequestMapping("/user/hello")
public String producthello(){
return "user hello";
}
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
测试使用
预期:打开http://localhost:8080/hello , 会自动跳转到/login页面,输入admin/admin用户名密码,可以访问/hello、/admin/hello、/user/hello,如果密码错误,/login页面会报错,如果使用user/user的用户密码组,除了/admin/hello不能访问。
结果:测试符合预期
二、使用数据库存储用户案例
我们真正使用安全框架的时候,不可能把用户名密码配置在内存中,这个时候,就是根据登陆的用户名和密码,到数据中进行查询,如果匹配,就登陆成功并加载权限等,如果不匹配,就返回错误,在shiro中我们是通过自定义realm,在doAuth方法中,根据用户名查询用户信息,返回自定义用户信息,密码,以及配置了密码的加密方式,盐等。
在spring-security中,我们只需要提供一个实现标准接口UserDetailsService的loadUserByUsername(String username)方法,如果查询出用户就组装UserDetails接口的数据,没有抛异常,其他的就交给框架工作。使用感官上和shiro基本一致,区别:spring-security的用户和角色信息一次加载了。
1、通过 auth.userDetailsService(userDetailsService) 配置我们自己实现的userDetailsService
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
2、userDetailsService
@Component("userDetailsService")
public class UserDetailsServiceImple implements UserDetailsService{
/** security中ROLE判断的默认前缀ROLE_, 你不加,它对比的时候会加。*/
private static final String role_prefix = "ROLE_";
private UserDao userDao = new UserDao();
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在!");
}
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role_prefix+user.getRole());
grantedAuthorities.add(grantedAuthority);
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), grantedAuthorities);
}
}
3、UserDao 这个是模拟的数据库数据,有admin/admin role=admin和user/user role=role的2个用户,另外因为我们加密使用的是spring-security的加密方式,那么数据库存的密码就需要用spring-security的加密进行处理。
/**
* 数据库操作的模拟
*/
public class UserDao {
private static Map<String,User> users = new HashMap<String,User>();
static {
User user1 = new User();
user1.setUsername("admin"); // 密码是123456采用
user1.setPassword("$2a$10$XLO0nZFBvLguTssPZdYr1ueQeiCYztmlKmh3J5XPLVOALuXRCzVX6");
user1.setUsername("admin");
user1.setRole("admin");
User user2 = new User();
user2.setUsername("user");
user2.setPassword("$2a$10$XLO0nZFBvLguTssPZdYr1ueQeiCYztmlKmh3J5XPLVOALuXRCzVX6");
user2.setRole("user");
users.put(user1.getUsername(), user1);
users.put(user2.getUsername(), user2);
}
public User findUserByUsername(String username) {
User user = users.get(username);
return user;
}
/**
* spring-security加密的测试。
*/
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123456");
System.out.println(password);
}
}
4、controller上我们主要增加了/center的请求方法。
/**
* SecurityContextHolder.getContext().getAuthentication().getPrincipal() 获取登陆用户
*/
@RequestMapping("/center")
public String center() {
String userDetails = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
userDetails = ((UserDetails) principal).getUsername();
userDetails = ((UserDetails) principal).getAuthorities().toString();
} else {
userDetails = principal.toString();
}
return userDetails;
}
userDetails = SecurityContextHolder.getContext().getAuthentication().getPrincipal() 即我们认证时loadUserByUsername方法访问的用户认证信息。
5、配置类MySecurityConfiguration
/**
* Security的配置类
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启支持注解方式的权限细粒度配置
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
// 添加一个userDetailService通过数据库认证,并加载角色信息, 以及设置加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
// 配置路径的拦截
// produect 需要user权限
// admin 需要admin权限
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("user")
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated() //
.and()
.formLogin()
.and()
.httpBasic()
.and().logout().logoutUrl("/logout")
.and().csrf().disable();
}
}
测试使用
预期:打开http://localhost:8080/hello , 会自动跳转到/login页面,输入admin/admin用户名密码,可以访问/hello、/user/hello,如果密码错误,/login页面会报错,如果使用user/user的用户密码组,则/admin/hello不能访问。另外/center页面可以获取用户认证后的信息。
结果:测试符合预期
三、JWT的方式
1、主要操作
使用jwt的方式,我们在认证这块还是一样的,即使用原有的密码用户进行登陆,但是,不使用session进行存储用户信息
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态session
那么我们已经登陆后,怎么才能够获取用户是否登陆的这个认证后信息呢? 主要是采用一个拦截器,在默认的认证拦截器前运行,通过req.getHeader(),获取自定义的auth中带的token字符串(这个token是我们在登陆成功后,返回前端保存的)。
protected void configure(HttpSecurity http) throws Exception {
.and().addFilterBefore(new JWTFilter(jwtService), UsernamePasswordAuthenticationFilter.class);
public class JWTFilter extends GenericFilterBean {
private final static String HEADER_AUTH_NAME = "auth";
private JwtService jwtService;
public JWTFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);
String parameterToken = httpServletRequest.getParameter(HEADER_AUTH_NAME);
if(StringUtils.isNotEmpty(parameterToken)) { // 兼顾param上取token值,方便获取。
Authentication authentication = jwtService.getAuthentication(parameterToken);
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} else {
if (StringUtils.isNotEmpty(authToken)) {
Authentication authentication = jwtService.getAuthentication(authToken);
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
/**
* 认证成功处理
*/
private class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println(jwtService.createToken(authentication));
}
}
2、jwt的操作类
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
public class JWTUtils {
/**
* 私有key,加密是用户名密码的基础上再加上private_key
*/
private static final String private_key = "abcdef";
/**
* token刷新时间 , 默认半小时
*/
private static final long token_expire_time = 30 * 60 * 1000 ;
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + token_expire_time);
Algorithm algorithm = Algorithm.HMAC256(private_key + secret);
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
public static boolean verify(String token, String username, String secret) {
Algorithm algorithm = Algorithm.HMAC256(private_key + secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
try {
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException e) {
return false;
}
return true;
}
public static String getUsername(String token) {
return getClaim(token,"username");
}
public static String getClaim(String token,String key) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(key).asString();
}
}
/**
* jwt认证相关
*/
@Service
public class JwtService {
private UserDao userDao = new UserDao();
public String createToken(Authentication authentication) {
String username = authentication.getName(); // 实际从userDetails中获取usernmae
User user = userDao.findUserByUsername(username);
String sign = JWTUtils.sign(username, user.getPassword());
return sign;
}
public Authentication getAuthentication(String token) {
String username = JWTUtils.getUsername(token);
User user = userDao.findUserByUsername(username);
if (user == null) {
return null;
}
boolean verify = JWTUtils.verify(token, username, user.getPassword());
if(!verify) {
return null;
}
MyUser myUser = new MyUser(user);
Collection<? extends GrantedAuthority> authorities = myUser.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, token, authorities);
}
}
3、pojo和dao等,UserDao类不变、User类不变,案例增加了一个MyUser的user包装类,实现了UserDetails,不是必须。
public class MyUser implements UserDetails {
private String username;
private String password;
private String role;
private static final long serialVersionUID = -601304311090873229L;
public MyUser() {
}
public MyUser(User user) {
this.username = user.getUsername();
this.password = user.getPassword();
this.role = user.getRole();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (role != null) {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_"+this.role));
}
return Arrays.asList(new GrantedAuthority[0]);
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
4、配置类
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtService jwtService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态session
.and()
.authorizeRequests()
.antMatchers("/user/**").hasRole("user")
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated()
.and()
// 配置登陆和登陆成功的处理
.formLogin().loginProcessingUrl("/login").successHandler(this.new MyAuthenticationSuccessHandler())
.and().csrf().disable()
.httpBasic()
// 配置登出和登出处理器
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandlers())
// 在UsernamePasswordAuthenticationFilter之前执行我们添加的JWTFilter
.and().addFilterBefore(new JWTFilter(jwtService), UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/swagger-resources/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/webjars/**")
.antMatchers("/v2/**")
.antMatchers("/h2-console/**");
}
/**
* 认证成功处理
*/
private class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println(jwtService.createToken(authentication));
}
}
/**
* 退出处理
*/
private static class LogoutSuccessHandlers implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("退出登陆");
out.flush();
}
}
}
5、测试使用
预期:用admin/123456登陆,登陆成功后,会返回token信息,然后带上token,依次访问/hello?auth=token /admin/hello?auth=token /user/hello?auth=token,预期admin用户不能访问/user/hello。
结果:测试符合预期
四、 注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
//hasRole和hasAuthority都会对UserDetails中的getAuthorities进行判断区别是hasRole会对字段加
// 上ROLE_后再进行判断
@RequestMapping("/test")
@PreAuthorize("hasRole('admin')") // 需要admin的角色
// @PreAuthorize("hasAuthority('ROLE_admin')") //
public String test() {
return "id";
}
五、spring-security主要的介绍
SecurityContext :安全的上下文,所有的数据都是保存到SecurityContext中,通过SecurityContext获取Authentication。
SecurityContextHolder:用来获取SecurityContext中保存的数据的工具 SecurityContext context = SecurityContextHolder.getContext();
Authentication:表示当前的认证情况,如UserDetails、Credentials、isAuthenticated、Principal。
UserDetails:getPassword、getUsername等
UserDetailsService:用来自己自定义查询UserDetails信息
AuthenticationManager:AuthenticationManager用来进行验证,如果验证失败会抛出相对应的异常
PasswordEncoder: 密码加密器
BCryptPasswordEncoder:哈希算法加密
NoOpPasswordEncoder:不使用加密