Spring Security的账号密码登录+Azure AD的授权登录集成Demo

一、项目准备:

1.创建一个Springboot项目。

2.注册一个微软的Azure AD服务,并且注册应用,创建用户。

springboot项目pom文件如下:



    4.0.0

    
        org.springframework.boot
        spring-boot-starter-parent
        2.7.2
        
    

    com.framework
    security-azure-test
    1.0-SNAPSHOT
    Demo project for Spring Boot

    
        11
        4.7.0
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            org.springframework.boot
            spring-boot-starter-security
        

        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        

        
            org.springframework.boot
            spring-boot-starter-oauth2-client
        

        
            com.azure.spring
            spring-cloud-azure-starter-active-directory
        

        
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        

        
        
            mysql
            mysql-connector-java
        

        
        
            org.slf4j
            slf4j-api
            1.7.25
        

        
            org.slf4j
            slf4j-log4j12
            1.7.25
        

        
            org.apache.commons
            commons-lang3
            3.5
        

        
            org.projectlombok
            lombok
            1.18.2
            provided
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
        
            
                com.azure.spring
                spring-cloud-azure-dependencies
                ${spring-cloud-azure.version}
                pom
                import
            
        
    

二、构建SpringSecurityConfig

这里在HttpSecurity需要配置常规登录选项,并且同时使用oauth2Login登录选项。

1.在authorizationManagerBuilder中构建自定义的一个Provider。

2.在httpSecurity构建常规账号密码登录的选项。

3.在httpSecurity构建oauth2login授权登录选项。

4.在httpSecurity构建Oauth2LoginConfigurer,并且实现自定义实现Oauth2UserService,来完成用户角色权限的构建。

5.在httpSecurity添加授权认证成功后的handler实现,用于重定向授权后的登录成功接口。

代码如下:

/**
 * @Author: LongGE
 * @Date: 2023-05-12
 * @Description:
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 在授权成功后查询本地数据库用户以及角色和权限信息。
     */
    @Autowired
    private CustomOidcService customOidcService;

    /**
     * 自定义的provider,用于账号密码登录
     */
    @Autowired
    private CustomDaoAuthenticationProvider customDaoAuthenticationProvider;

    /**
     * 自定义在授权成功后,控制授权登录成功后跳转本地项目的页面和接口,并且也可以用于添加session和cookie
     */
    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    /**
     * 密码校对验证器
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 构建manager认证器
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 添加自定义的provider,通过自定义的provider可以实现不同的账号密码登录
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        authenticationManagerBuilder.authenticationProvider(customDaoAuthenticationProvider);
    }

    /**
     * 构建HttpSecurity 认证
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/login/oauth2/code/azure").permitAll()
                .antMatchers("/AuthLoginController/**").permitAll()
                .anyRequest().authenticated()
                .and()
                //构建UsernamePasswordAuthenticationFilter拦截器
                .formLogin()
                .loginPage("/login").permitAll()
                .and()
                //构建OAuth2LoginConfigurer,用于OAuth2Login授权登录
                .oauth2Login()
                .loginPage("/login").permitAll()
                //授权服务器UserInfo端点的配置选项。
                .userInfoEndpoint()
                //添加一个自定义的OAuth2UserService,用于实现授权成功后对用户信息和角色权限信息的封装
                .oidcUserService(customOidcService)
                .and()
                //添加一个Handler,用于授权成功后,对跳转登录成功后的重定向页面进行指向,也可以用于添加授权登录成功的sessionID和Cookie
                .successHandler(customAuthenticationSuccessHandler);
    }

    /**
     * 过滤静态页面和图片信息,不让Filter拦截
     * @param web
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/assets/images/**");
    }
}

三、自定义CustomDaoAuthenticationProvider

自己实现AuthenticationProvider接口,这样可以根据自己传入的不同TAuthenticationToken去执行自己定义Provider,可以更加灵活自主的实现登录业务逻辑。

/**
 * @Author: LongGE
 * @Date: 2023-04-10
 * @Description:
 */
@Component
@Slf4j
public class CustomDaoAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailsServiceImpl customUserDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        CustomUserDetails customUserDetails = (CustomUserDetails) customUserDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
        CustomDaoUsernameToken customDaoUsernameToken = new CustomDaoUsernameToken(customUserDetails,null, customUserDetails.getAuthorities());
        return customDaoUsernameToken;
    }

    /**
     * As a business judgment, built in the controller,
     * the judgment is made here so that you can call the AuthenticationProvider that encapsulates the corresponding one in ProviderManeger
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class authentication) {
        return CustomDaoUsernameToken.class.isAssignableFrom(authentication);
    }
}

四、自定义CustomDaoUsernameToken

继承AbstractAuthenticationToken抽象类,自己定义一个AuthenticationToken类,这样在登录时候调用authenticate()方法时候传入自己定义的AuthenticationToken就可以,这样ProviderManager类就会自动匹配自定义的Provider去实现登录认证逻辑。

/**
 * @Author: LongGE
 * @Date: 2023-04-10
 * @Description:
 */
public class CustomDaoUsernameToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    private Object credentials;

    public CustomDaoUsernameToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    public CustomDaoUsernameToken(Object principal, Object credentials,
                                               Collection authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }


    @Override
    public Object getCredentials() {
        return credentials;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

五、自定义CustomUserDetailsServiceImpl

自定义的登录认证,实现UserDetailService接口,在provider中会调用自定义的CustomUserDetailsServiceImpl类的loadUserByUsername()方法来认证账号是否存在并且查询用户角色以及权限信息,并且封装到了Security的上下文中,后续方法可以直接在上线文中回去这些用户信息。

@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {

	private static final Logger LOGGER = LoggerFactory.getLogger(CustomUserDetailsServiceImpl.class);

	@Autowired
	private SystemUserDao systemUserDao;

	@Override
	public UserDetails loadUserByUsername(String username) throws BadCredentialsException {

		LOGGER.debug("CustomUserDetailsServiceImpl: " + ":loadUserByUsername()={}", username);

		User user = new User();
		Set hasAuthority = new HashSet<>();
		SystemUser systemUser = systemUserDao.queryByUsername(username);
		user.setId(systemUser.getId());
		user.setUsername(username);
		user.setEnabled(true);
		user.setAuthorities(hasAuthority);
		return new CustomUserDetails(user);
	}
}

六、自定义CustomOidcService

在AzureAD授权认证后,返回给我们用户信息,由OAuth2LoginAuthenticationFilter拦截器拦截,调用attemptAuthentication()方法,在此方法中会获取ProviderManager类,在调用ProviderManager的authenticate()方法进行认证,传入的参数是OAuth2LoginAuthenticationToken类型的token,在封装在ProviderManager中只有OidcAuthorizationCodeAuthenticationProvider类满足认证条件,在此provider的authenticate()方法中会调用自定义的CustomOidcService类的loadUser()方法进行认证,传入的参数是OidcUserRequest类型,在这里通过userRequest.getIdToken();方法获取OidcIdToken,这里封装AzureAD中的基础用户信息,通过用户信息去数据库查询用户角色和权限,将角色和权限封装到Security的上下文中,并且也可以封装到redis等缓存中,方便后续使用。

/**
 * @Author: LongGE
 * @Date: 2023-05-15
 * @Description:
 */
@Slf4j
@Service
public class CustomOidcService implements OAuth2UserService {

    @Autowired
    private SystemUserDao systemUserDao;

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcIdToken idToken = userRequest.getIdToken();
        log.info("打印请求参数: {}",idToken);
        Set authorityStrings = new HashSet<>();
        Set authorities = authorityStrings.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
        SystemUser systemUser = systemUserDao.queryByUsername(userRequest.getIdToken().getPreferredUsername());
        CustomOidcUser customOidcUser = new CustomOidcUser(authorities, idToken, systemUser);
        return customOidcUser;
    }
}

七、自定义CustomAuthenticationSuccessHandler

在第六步认证成功后,AbstractAuthenticationProcessingFilter拦截器,会调用AuthenticationSuccessHandler接口的successfulAuthentication()方法,自定义的CustomAuthenticationSuccessHandler类是实现了这个接口的successfulAuthentication()方法,实现此方法主要是用户在用户通过AzureAD授权登录成功后,可以控制用户去加载登录成功后的浏览页面,并且还需要给前端返回的Response中添加Http请求头中添加cookie,这样以后前端每次访问后端接口,都携带此cookie那么就可以通过拦截器去确认用户是否登录。

/**
 * @Author: LongGE
 * @Date: 2023-05-22
 * @Description:    用户认证成功后处理后续重定向操作的
 * Strategy used to handle a successful user authentication.
 * 

* Implementations can do whatever they want but typical behaviour would be to control the * navigation to the subsequent destination (using a redirect or a forward). For example, * after a user has logged in by submitting a login form, the application needs to decide * where they should be redirected to afterwards (see * {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be * included if required. */ @Service @Slf4j public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { onAuthenticationSuccess(request, response, authentication); chain.doFilter(request, response); } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { CustomOidcUser customOidcUser = (CustomOidcUser)authentication.getPrincipal(); SystemUser user = customOidcUser.getSystemUser(); // Session ID String sessionId = UUID.randomUUID().toString(); Map tokenClaims = new HashMap<>(); tokenClaims.put("SessionId", sessionId); //Create token //Token newAccessToken = tokenProvider.generateAccessToken(user.getUsername(), tokenClaims, authentication, tokenExpirationSec); //Enter token log //customBaseService.logToken(newAccessToken); /* if(user != null && user.getId() != null) { //Add Session Id to UserSession DB customBaseService.addUserSession(user.getId(), sessionId, request); //Add Redis cache with expiration time customBaseService.addRedisUserSession(user.getId(), user.getUsername()); } //Set the redirect path and add the token cache to the cookie response.addHeader("Set-Cookie", cookieUtil.createAccessTokenCookie(newAccessToken.getTokenValue(), newAccessToken.getDuration()).toString());*/ response.sendRedirect("/index"); } }

八、登录页面

登录页面支持简单的账号密码登录,同时也支持AzureAD的授权方式登录。



    
    
    
    
    
    
        
        Title
    
    
        

用户登录

用户名:
密码:






login.js的js代码:

$(document).ready(function() {

	document.getElementById("password").addEventListener("keyup", function(event) {
		if (event.keyCode === 13) {
			$('#loginbtn').click();
			return false;
		}
	});

	//LDAP Login
    $('#ldaploginbtn').click(function() {
        $('#errorMessage').text('');
        $('#divError').hide();

        //Check account password
        let $name=$('#username');
        let $pwd=$('#password');
        // 按钮点击后检查输入框是否为空,为空则找到span便签添加提示
        if ($name.val().length===0 || $name.val() == ("") || $pwd.val().length===0 || $pwd.val() == ("")) {
            $('#errorMessage').text('Please fill in the account password!');
            $('#divError').show();
        }else {
            var formData = $("#loginform").serializeJSON();
            var jsonData = JSON.stringify(formData);

            $.ajax({
                url: "AuthLoginController/doLogin",
                type: 'POST',
                data: jsonData,
                contentType: 'application/json; charset=utf-8',
                dataType: 'json',
                success: function(data) {
                    if (data.status == "SUCCESS") {
                        console.log("登录成功返回!")
                        window.location.href = data.redirectPath;//"/index";
                    } else {
                        $('#errorMessage').text(data.message);
                        $('#divError').show();
                    }
                },
                error: function(xhr, ajaxOptions, thrownError) {
                    swalexceptionhandler(xhr.status, xhr.responseText);
                }
            });
        }
    });
});

function swalexceptionhandler(status, responseText) {
    if (status == "412" || status == "422") {
        var obj = JSON.parse(responseText);
        var displaymsg = "";
        for (let i = 0; i < obj.errors; i++) {
            displaymsg += obj.errorInfo[i].errCode + ":" + obj.errorInfo[i].errDescription + " (" + obj.errorInfo[i].errField + ")" + "
"; } //swal('Validation', displaymsg, 'warning'); } else { //swal('Exception', responseText, 'error'); } }

九、登录接口AuthLoginController与LoginController

LoginController:主要加载登录页面和登录成功页面。

AuthLoginController:处理简单的账号密码登录请求逻辑。

代码分别如下:

/**
 * @Author: LongGE
 * @Date: 2023-05-19
 * @Description:
 */
@Controller
@Slf4j
public class LoginController {

    @RequestMapping("/login")
    public String loginHtml(){
        return "login";
    }

    @RequestMapping("/index")
    public String indexHtml() {
        log.info("发送请求违背拦截!");
        return "index";
    }
}
/**
 * @Author: LongGE
 * @Date: 2023-05-12
 * @Description:
 */
@RestController
@RequestMapping("/AuthLoginController")
@Slf4j
public class AuthLoginController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private ServletContext context;

    @PostMapping("/doLogin")
    public ResponseEntity auth(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
        log.info("开始登录! username={}, password={}", loginRequest.getUsername(), loginRequest.getPassword());
        Authentication authentication = authenticationManager.authenticate(
                new CustomDaoUsernameToken(loginRequest.getUsername(), loginRequest.getPassword()));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        log.info("登录成功! {}", authentication);
        HttpHeaders responseHeaders = new HttpHeaders();
        String loginPath = context.getContextPath() + "/index";
        LoginResponse loginResponse = new LoginResponse(LoginResponse.SuccessFailure.SUCCESS, "Auth successful. Tokens are created in cookie.", loginPath);
        return ResponseEntity.ok().headers(responseHeaders).body(loginResponse);
    }
}

总结:

附一张授权登录的基础流程图:

Spring Security的账号密码登录+Azure AD的授权登录集成Demo_第1张图片

 

 

你可能感兴趣的:(spring,azure,java)