登录功能

前端要登录,需要调用登录接口,因此后端必须要有LoginController。
在这里插入图片描述
先看LoginController中的 /login 登录功能的实现。

	@ApiOperation(value = "登录之后返回token")
    @PostMapping("/login")
    public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
        return adminService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword(), adminLoginParam.getCode(), request);
    }

RespBean是公共返回对象,里面定义了code、message和obj(Object对象)。AdminLoginParam是前端传来的登录参数,包括用户名、密码和验证码。HttpServletRequest request是什么?
再看adminService中的login方法。

//登录之后返回token
    @Override
    public RespBean login(String username, String password, String code, HttpServletRequest request) {
        String captcha=(String) request.getSession().getAttribute("captcha");
        if (StringUtils.isEmpty(code) || !captcha.equalsIgnoreCase(code)){
            return RespBean.error("验证码输入错误,请重新输入");
        }
        UserDetails userDetails=userDetailsService.loadUserByUsername(username);
        if (userDetails==null || !passwordEncoder.matches(password, userDetails.getPassword())){
            return RespBean.error("用户名或密码不正确");
        }
        if (!userDetails.isEnabled()){
            return RespBean.error("账号被禁用,请联系管理员!");
        }
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String, String> tokenMap=new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return RespBean.success("登录成功", tokenMap);
    }

该方法是先从request中取出验证码,和用户输入的验证码(code)进行对比。然后使用userDetailsService根据username获取userDetails,即用户信息。如果获取不到,或者输入的password和userDetails中的password不同,则返回错误信息。
校验通过后,根据userDetails和调用getAuthorities()方法(用来获取此用户所拥有的权限),生成authenticationToken(和jwt生成的token有什么不同?),并设置到SecurityContextHolderAuthentication中。
然后使用jwtTokenUtil工具类(之后会介绍),根据userDetails生成token,并返回给前端,表示登录成功。

再看看退出登录功能。

@ApiOperation(value = "退出登录")
    @PostMapping("/logout")
    public RespBean logout(){
        return RespBean.success("注销成功");
    }

直接返回退出成功的消息。因为这一部分主要是前端来做的。
在登录之后,后端会生成一个token令牌返回给前端,前端每次调用后端接口时都会携带这个token,当执行退出登录后,前端把这个token删除掉,此时如果前端再次请求后端接口,由于Spring Security有拦截器去校验token,发现token非法会拦截请求,这样就实现了退出登录的功能。

LoginController中还有一个获取当前登录用户信息的方法。

@ApiOperation(value = "获取当前登录用户的信息")
    @GetMapping("/admin/info")
    public Admin getAdminInfo(Principal principal){
        if (principal==null){
            return null;
        }
        String username = principal.getName();
        Admin admin=adminService.getAdminByUserName(username);
        admin.setPassword(null);
        admin.setRoles(adminService.getRoles(admin.getId()));
        return admin;
    }

这个方法的关键是传入的principal,这个principal是由框架自动赋值传入的(不是先@Autowired),应该只能在controller中使用。(自动赋值传入这件事总感觉很不可思议。。)
以上是LoginController中的内容。用到了jwtTokenUtil工具类和访问拦截,因为访问拦截是Spring Security框架做的,因此没有显式地出现在上述代码中。下面介绍一下。
首先是jwtTokenUtil工具类,这个工具类实现了几种方法:1、根据用户信息(userDetails)生成token;2、从token中获取登录用户名;3、验证token是否有效(token是否过期;token中的username是否与userDetails中的username一致);4、判断token是否可以被刷新;5、刷新token。
在这里插入图片描述

先看一下SecurityConfig类,这个是Spring Security的配置类,继承了WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IAdminService adminService;
    @Autowired
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private CustomFilter customFilter;
    @Autowired
    private CustomUrlDecisionManager customUrlDecisionManager;

    @Override
    @Bean
    public UserDetailsService userDetailsService(){
        。。。
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		。。。
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		。。。
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        。。。
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    }

}
@Override
    @Bean
    public UserDetailsService userDetailsService(){
        return username -> {
            Admin admin = adminService.getAdminByUserName(username);
            if (admin!=null){
                admin.setRoles(adminService.getRoles(admin.getId()));
                return admin;
            }
            throw new UsernameNotFoundException("用户名或密码不正确");
        };
    }

UserDetailsService接口和UserDetails是Spring Security提供给我们的,里面只有一个方法UserDetails loadUserByUsername(String str),因此这里用了lambda表达式来实现这一方法,最后返回admin(Admin实现了UserDetails)。然后注入到容器中,方便其它地方使用。注意,userDetailsService里已经查找了该用户的roles并赋值,用于权限控制(getAuthorities())。

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_admin")
@ApiModel(value="Admin对象", description="")
public class Admin implements Serializable, UserDetails {

	。。。

    @Override
    @JsonDeserialize(using = CustomAuthorityDeserializer.class)
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities=roles
                .stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());
        return authorities;
    }

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

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

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

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

上面的Admin类实现了UserDetails接口,getAuthorities()方法用于返回此用户所拥有的权限。这里调用了CustomAuthorityDeserializer反序列化,和登录功能无关,是后面实现的其它功能用到的。

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

这个是SecurityConfig下的configure方法,里面传入了AuthenticationManagerBuilder,这个方法的作用应该是指定所使用的userDetailsService和passwordEncoder(因为这两个都已经重新配置过了)。

@Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/login",
                "/logout",
                "/css/**",
                "/js/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/captcha",
                "/ws/**"
        );
    }

这个同样是SecurityConfig下的configure方法,传入了WebSecurity,用来配置对哪些访问路径忽略拦截。

@Override
protected void configure(HttpSecurity http) throws Exception {
    //使用jwt,不需要csrf
    http.csrf()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated()
            //动态权限配置
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {

                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setAccessDecisionManager(customUrlDecisionManager);
                    o.setSecurityMetadataSource(customFilter);
                    return o;
                }

            })
            .and()
            .headers()
            .cacheControl();

    http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

    http.exceptionHandling()
            .accessDeniedHandler(restfulAccessDeniedHandler)
            .authenticationEntryPoint(restAuthorizationEntryPoint);

}

这个同样是SecurityConfig下的configure方法,传入了HttpSecurity,进行了相关配置。配置主要包括:1、禁用csrf;2、基于token,不需要session,不创建HttpSession,不使用HttpSession来获取SecurityContext;3、所有请求都要求认证;4、动态权限配置,设置了自定义的AccessDecisionManager(访问决策管理器)SecurityMetadataSource(安全元数据源);5、禁用缓存;6、在UsernamePasswordAuthenticationFilter前配置jwtAuthenticationTokenFilter(这个是自己定义的filter,后面有讲);7、配置异常控制,accessDeniedHandler用来控制认证过的用户访问无权限资源时的异常,authenticationEntryPoint用来控制匿名用户访问无权限资源时的异常,这里说的异常应该是Spring Security拦截到非法请求后执行的方法。

下面是自定义的异常控制。

//当访问接口没有权限时,自定义返回结果
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        PrintWriter out = httpServletResponse.getWriter();
        RespBean bean=RespBean.error("权限不足,请联系管理员");
        bean.setCode(403);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }

}
//当未登录或token失效访问接口时,自定义的返回结果
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        PrintWriter out = httpServletResponse.getWriter();
        RespBean bean=RespBean.error("尚未登录,请先去登录");
        bean.setCode(401);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }

}

下面是JwtAuthenticationTokenFilter,继承了OncePerRequestFilter

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = httpServletRequest.getHeader(tokenHeader);
        if (authHeader!=null && authHeader.startsWith(tokenHead)){
            String authToken = authHeader.substring(tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            if (username!=null && SecurityContextHolder.getContext().getAuthentication()==null){
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)){
                    UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

}
jwt:
  tokenHeader: Authorization
  secret: yeb-secret
  expiration: 604800
  tokenHead: Bearer

前端的请求头里包含key-value对,其中一个key就是Authorization,value就是Bearer+空格+jwt令牌,用来进行权限认证。先从HttpServletRequest中拿到Authorization对应的value,是个字符串,把jwt令牌截取出来,使用jwtTokenUtil工具类根据token获取username。这个filter的作用是如果token对应的username确实存在,而且token没过期,但没登录(Spring Security上下文里找不到),就让它先登录再放行。(既然还没登录,前端哪来的token?设置这个filter应该是为了方便测试接口)

//权限控制 根据请求的url得出请求所需的角色
@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private IMenuService menuService;

    AntPathMatcher antPathMatcher=new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<Menu> menus=menuService.getMenusWithRole();
        for (Menu menu:menus) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)){
                String[] str=menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
                return SecurityConfig.createList(str);
            }
        }
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }

}
//权限控制 判断用户角色
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            //当前url所需角色
            String needRole=configAttribute.getAttribute();
            //判断角色是否为登录即可访问的角色,此角色在CustomFilter中设置
            if (needRole.equals("ROLE_LOGIN")){
                //判断是否登录
                if (authentication instanceof AnonymousAuthenticationToken){
                    throw new AccessDeniedException("尚未登录,请登录");
                }else {
                    return;
                }
            }
            //判断用户角色是否为url所需角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities){
                if (authority.getAuthority().equals(needRole)){
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }

}

在这里插入图片描述

SecurityMetadataSource(安全元数据源)的作用是根据请求的url和哪个menu匹配,进而在t_menu_role表中查出这个menu对应哪些角色,然后把这些角色名作为权限设置到Collection_ConfigAttribute中。简单来说就是设置本次url请求所需要的具体权限。
AccessDecisionManager(访问决策管理器)的作用是判断当前用户的权限列表(这个当前用户的权限列表是在登录时随着userDetails设置到Spring Security上下文中的)中是否有本次url请求所需要的权限。简单来说就是访问权限决策。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值