SpringBoot整合Spring Security(二)

实现自定义表单登录

1.实现自定义UserDetailsService

需要重写 loadUserByUsername 方法,参数是用户输入的用户名。在此,我们可以实现自定义的登录逻辑。

@Component(value = "myUserDetailService")
public class MyUserDetailServiceImpl implements UserDetailsService {

    /**
     * 用户Service
     */
    private final UserService userService;

    /**
     * 用户角色Service
     */
    private final UserRoleService userRoleService;

    /**
     * 角色Service
     */
    private final RoleService roleService;

    /**
     * 角色权限Service
     */
    private final RoleAuthorityService roleAuthorityService;

    /**
     * 权限Service
     */
    private final AuthorityService authorityService;
    
    @Autowired
    public MyUserDetailServiceImpl(UserService userService, UserRoleService userRoleService,
                                   RoleService roleService, RoleAuthorityService roleAuthorityService,
                                   AuthorityService authorityService) {
        this.userService = userService;
        this.userRoleService = userRoleService;
        this.roleService = roleService;
        this.roleAuthorityService = roleAuthorityService;
        this.authorityService = authorityService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //这里可以可以通过username(登录时输入的用户名)然后到数据库中找到对应的用户信息,并构建成我们自己的UserInfo来返回。
        User user = userService.findUserByName(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("userName not found");
        }

        //角色授权:授权代码需要加ROLE_前缀,controller上使用时不要加前缀
        //权限授权:设置和使用时,名称保持一至即可
        List<Integer> roleIdList = userRoleService.findRoleIdsByUserId(user.getUserId());
        List<Role> roleList = roleService.findByRoleIdIn(roleIdList);
        String role = "";
        StringBuilder roleSb = new StringBuilder();
        roleList.forEach(e -> roleSb.append(",").append("ROLE_").append(e.getRoleCode()));
        if (roleSb.length() > 0) {
            role = roleSb.substring(1);
        }

        List<Integer> authorityIdList = roleAuthorityService.getAuthorityIdsByRoleIds(roleIdList);
        List<Authority> authorityList = authorityService.getAuthority(authorityIdList);
        String authority = "";
        StringBuilder authoritySb = new StringBuilder();
        authorityList.forEach(e -> authoritySb.append(",").append(e.getAuthorityCode()));
        if (authoritySb.length() > 0) {
            authority = authoritySb.substring(1);
        }

        return new UserDto(user.getUserId(), user.getUserName(), user.getUserPassword(), user.getUserSalt(),
                role + authority, true, true, true, true);
    }
}

输入账号密码后,security会调用此方法,通过用户名查找此用户是否存在,最后面我没有直接返回security的User,而是返回了自定义的实体类,代码如下:

public class UserDto extends BaseDto implements UserDetails, Serializable {

    private static final long serialVersionUID = 1L;

    /** 用户ID */
    @Getter
    private Integer userId;

    /** 用户名 */
    private String username;

    /** 用户密码 */
    private String password;

    /** 加密盐 */
    @Getter
    private String salt;

    /** 角色 */
    private String role;

    /** 账户是否过期 */
    private boolean accountNonExpired;

    /** 账户是否被锁定 */
    private boolean accountNonLocked;

    private boolean credentialsNonExpired;

    private boolean enabled;

    public UserDto(Integer userId, String username, String password, String salt, String role,
                   boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.salt = salt;
        this.role = role;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

2.Security全局配置

@Configuration
@EnableWebSecurity //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 表单认证配置
     */
    private final FormAuthenticationConfig formAuthenticationConfig;

    /**
     * 验证用户Service
     */
    private final UserDetailsService userDetailsService;


    @Autowired
    public SecurityConfig(FormAuthenticationConfig formAuthenticationConfig, @Qualifier(value = "myUserDetailService") UserDetailsService userDetailsService) {
        this.formAuthenticationConfig = formAuthenticationConfig;
        this.userDetailsService = userDetailsService;
    }

    /**
     * 登录配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //不需要拦截的路径
                .antMatchers("/authentication/require").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();

        //表单登录配置
        formAuthenticationConfig.configure(http);
    }

    /**
     * 权限校验配置
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用自定义userDetailsService进行登录校验
        auth.userDetailsService(userDetailsService)
                //加密工具
                .passwordEncoder(passwordEncoder());
    }

    /**
     * 加密工具(指定了密码的加密方式(5.0 版本强制要求设置))
     *
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder(){
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        };
    }
}

2.1.formAuthenticationConfig

在这里将表单登录配置抽取出来,单独放在一个类里

@Component
public class FormAuthenticationConfig {

    private final AuthenticationSuccessHandler authenticationSuccessHandler;

    private final AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    public FormAuthenticationConfig(@Qualifier(value = "myAuthenticationSuccessHandler") AuthenticationSuccessHandler authenticationSuccessHandler,
                          @Qualifier(value = "myAuthenticationFailureHandler") AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationSuccessHandler = authenticationSuccessHandler;
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                //当请求需要身份认证时,默认跳转的url
                .loginPage("/authentication/require")
                //默认的用户名密码登录请求处理url
                .loginProcessingUrl("/authentication/form")
                //登录成功处理器
                .successHandler(authenticationSuccessHandler)
                //登录失败处理器
                .failureHandler(authenticationFailureHandler);
    }
}

2.2.登录成功处理器

@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private final ObjectMapper objectMapper;

    public MyAuthenticationSuccessHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {

        log.info("登录成功");

        //返回数据
        response.setContentType("application/json;charset=UTF-8");
        String type = authentication.getClass().getSimpleName();
        ResultVo resultVo = ResultVo.builder().status(200).msg("登陆成功!").data(type).build();
        response.getWriter().write(objectMapper.writeValueAsString(resultVo));

    }
}

2.3. 登陆失败处理器

@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private final ObjectMapper objectMapper;


    public MyAuthenctiationFailureHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        logger.info("登录失败");

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=UTF-8");
        ResultVo resultVo = ResultVo.builder().status(403).msg("登陆失败!").data(exception.getMessage()).build();
        response.getWriter().write(objectMapper.writeValueAsString(resultVo));

    }
}

3.登录验证

在这里需要打开一个工具postman对项目进行验证

3.1.未登录测试

用户未登录
当用户未登录时访问项目接口,会跳转到自定义配置的路径上,因为这里并没有配置此路径接口,所有会报404错误。

3.2.登录错误测试

登录失败
紧接着,向/authentication/form发起登录请求,输入一个错误的密码,此时会调用登录失败处理器,返回错误信息。

3.3.登陆成功测试

登陆成功
最后,输入正确的用户和密码,此时会调用登录成功处理器,返回成功信息。
此时,再次访问项目接口,可以看到,访问成功!
在这里插入图片描述

4.关于用户名和密码

表单登录,默认的用户名和密码分别是username和password,如果不是这两个参数将会无法登录

4.1.表单配置中实现自定义用户名和密码参数

在这里插入图片描述

4.2. 在过滤器中设置参数

这个可以在UsernamePasswordAuthenticationFilter中看到
用户名密码参数
如果想要实现自定义参数可以继承此接口,将自定义过滤器加到security过滤链中即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值