SpringBoot整合Security

2022-11-15 13:34:07 浏览数 (1)

1 为SpringBoot添加Security支持

Security作为Spring的官方安全框架,自然为SpringBoot提供了起步依赖(Starter),有了起步依赖,我们只要添加少量的Java配置,就可以把Security集成到SpringBoot项目中。

1.1 添加 spring-boot-starter-security 依赖

代码语言:javascript复制
         <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-security</artifactId>

        </dependency>

添加上述依赖后再启动springboot,项目即得到security的保护

默认的登录用户名是“user”,默认密码在启动时输出在控制台中。

这些默认信息被定义在了源码 “org.springframework.boot.autoconfigure.security.SecurityProperties” 中。

1.2 自定义用户验证和授权

要自定义用户的验证和授权需要重写UserDetails接口和UserDetailsService接口,并把UserDetailsService的实现类注册到Security配置中。

(1)实现UserDetails

代码语言:javascript复制
public class UserDetailsImpl implements UserDetails {

    public UserDetailsImpl(User user, List<Role> roles) {

        this.username = user.getUsername();

        this.password = user.getPassword();

        this.status = user.getStatus();

        if(roles!=null) {

            this.authorities = roles.stream()

                                    .map(r->new SimpleGrantedAuthority(r.getName()))

                                    .collect(Collectors.toList());

        }

    }

    private String username;

    private String password;

    private List<GrantedAuthority> authorities=new ArrayList<GrantedAuthority>();

    private int status;

    //省略属性getter、setter

}

(2)实现UserDetailsService

代码语言:javascript复制
@Service("userDetailsService")  //托管到Spring容器Seucrity自动识别

public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired

    private UserMapper userDb;

    @Autowired

    private RoleMapper roleDb;

   

    @Override

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userDb.selectOne(new QueryWrapper<User>().eq("username", username));

        List<Role> roles = roleDb.findRolesByUserId(user.getId());

        return new UserDetailsImpl(user, roles);

    }

}

Security框架在重写验证授权时,必须指定密码加密方式,即使密码不做散列加密也需要配置一个空的加密器。

Security的配置信息可以配置在继承了“WebSecurityConfigurerAdapter”父类的配置类中,如下所示。

代码语言:javascript复制
@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //必须配置密码加密方式

    @Bean

    public PasswordEncoder  passwordEncoder() {

        return NoOpPasswordEncoder.getInstance();//这里不使用密码加密,配置空的加密器

    }

}

1.3 自定义登录页面

重写 WebSecurityConfigurerAdapter 的 config(HttpSecurity http) 方法,可以实现Security的请求配置。

代码语言:javascript复制
@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...... 

    //配置Security细节

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()                            //配置登录

                .loginPage("/login")

                .loginProcessingUrl("/checklogin")

                .defaultSuccessUrl("/index")

            .and().logout()                         //配置注销

                .logoutUrl("/logout")

                .logoutSuccessUrl("/login?logout")  //注销后跳转的路径     

            .and().csrf().disable();                //禁用csrf令牌

    }

}

1.4 基于URL请求拦截

config(HttpSecurity http) 方法同样可以配置基于URL的请求拦截。

代码语言:javascript复制
@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...... 

    //配置Security细节

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()

                .antMatchers("/admin/**").hasAuthority("管理员")    // admin目录的所有请求需要管理员权限

                .anyRequest().authenticated()                       // 其它请求需要登录

            .and().formLogin()                         

                .loginPage("/login")                           

                .loginProcessingUrl("/checklogin")

                .defaultSuccessUrl("/index")

                .permitAll()    //由于上面配置了所有页面都需要登录,这里必须允许login/logout/index直接访问

            .and().logout()                            

                .logoutUrl("/logout")

                .logoutSuccessUrl("/login?logout")         

            .and().csrf().disable();                   

    }

}

1.5 基于方法注解的请求拦截

基于URL的请求拦截可能不够精确,尤其不能满足 RESTful API的需求,更合理的可能时基于方法进行注解授权。

(1)允许使用注解前置处理权限

代码语言:javascript复制
@Configuration

@EnableGlobalMethodSecurity(prePostEnabled = true)     //允许使用注解前置处理权限

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    ......

}

(2)在方法中添加注解@PreAuthorize,指定权限

代码语言:javascript复制
@Controller

@RequestMapping("/admin")

@PreAuthorize("hasAuthority('管理员')")

public class AdminController {

    @RequestMapping(value={"/","/index"})

    public String index(){

        return "admin-index";

    }

}

2 前后端分离项目的Security配置

对于前后端分离项目而言,后端不再返回页面,更不控制页面跳转,只返回JSON数据。与同步请求中的各种处理后的成功与失败跳转不同,Security需要针对异步请求提供成功或失败后的处理程序(Handler)。

2.1 Security的几种登录成功/失败处理程序:

前后端分离项目需要后端返回JSON数据而非页面,因此需要重写Security的几个处理程序:

(1)处理登录成功

http.formLogin().successHandler((req,resp,authentication)->{ }) 取代 http.formLogin().defaultSuccessUrl(String),设置登录成功后的处理

(2)处理登录失败

http.formLogin().failureHandler((req,resp,authException)->{ ... }) 取代 http.formLogin().failureUrl(String),设置登录失败后的处理

修改配置,实现登录成功(或失败)后使用JSON返回数据

(3)处理匿名(未登录)访问和权限不足请求

用户未登录时访问授权页面,Security会默认重定向到登录页,页面跳转不适用于前后端分离,因此需要授权异常机制。

http.exceptionHandling()

        .authenticationEntryPoint((req,resp,authException)->{ ... })

        .accessDeniedHandler((req,resp,denyEx)->{ ... })

(4)处理注销成功

http.formLogin().logoutSuccessHandler((req,resp,authentication)->{ }) 取代 http.formLogin().logoutSuccessUrl(url)

2.2 示例:

前后端分离项目的Security配置大致如下:

代码语言:javascript复制
   //配置Security细节

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()

                .anyRequest().authenticated()           //任意请求不允许匿名访问

            .and().formLogin()                                 

                .loginPage("/login")                    //不配置loginPage会生成默认登录页

                .loginProcessingUrl("/login")           //登录处理URL

                .successHandler((req,resp,auth)->{      //登录成功处理:返回登录用户信息

                    resp.setContentType("application/json;charset=utf-8");

                    String json = new ObjectMapper().writeValueAsString(auth.getPrincipal());

                    resp.getWriter().print(json);

                })

                .failureHandler((req,resp,authEx)->{    //登录失败处理:返回401状态码

                    resp.setContentType("application/json;charset=utf-8");

                    resp.setStatus(401);

                    String json = "{"message":"" authEx.getMessage() ""}";

                    resp.getWriter().print(json);

                })

                .permitAll()                            //允许上述路径被匿名访问

            .and().logout()                            

                .logoutUrl("/logout")

                .logoutSuccessHandler((req,resp,auth)->{//注销成功后处理

                    resp.setContentType("application/json;charset=utf-8");

                    String json = "{"message":"注销成功"}";

                    resp.getWriter().print(json);

                })

            .and().csrf().disable()

            .exceptionHandling()                        //自定义一些异常处理

                //许匿名访问时的处理,返回状态码401(等同于GET请求/login)

                .authenticationEntryPoint((req,resp,authException)->{  

                    resp.setContentType("application/json;charset=utf-8");

                    resp.setStatus(401);

                    String json = "{"message":"无法匿名访问,请先登录"}";

                    resp.getWriter().print(json);

                })

                //权限不足的访问处理,返回状态码403

                .accessDeniedHandler((req,resp,denyEx)->{

                    resp.setContentType("application/json;charset=utf-8");

                    resp.setStatus(403);

                    String json = "{"message":"" denyEx.getMessage() ""}";

                    resp.getWriter().print(json);

                });                

    }

0 人点赞